hassle.hassle API documentation

# hassle.hassle

View Source

      1import argparse
      2import os
      3import sys
      4
      5import isort
      6from gitbetter import Git
      7from pathier import Pathier
      8
      9from hassle import hassle_utilities
     10from hassle.generate_tests import generate_test_files
     11from hassle.run_tests import run_tests
     12
     13root = Pathier(__file__).parent
     14
     15
     16def get_args() -> argparse.Namespace:
     17    parser = argparse.ArgumentParser()
     18
     19    parser.add_argument(
     20        "package",
     21        type=str,
     22        default=".",
     23        nargs="?",
     24        help=""" The name of the package or project to use,
     25        assuming it's a subfolder of your current working directory.
     26        Can also be a full path to the package. If nothing is given,
     27        the current working directory will be used.""",
     28    )
     29
     30    parser.add_argument(
     31        "-b", "--build", action="store_true", help=""" Build the package. """
     32    )
     33
     34    parser.add_argument(
     35        "-t",
     36        "--tag_version",
     37        action="store_true",
     38        help=""" Add a git tag corresponding to the version in pyproject.toml. """,
     39    )
     40
     41    parser.add_argument(
     42        "-i",
     43        "--install",
     44        action="store_true",
     45        help=""" Install the package from source. """,
     46    )
     47
     48    parser.add_argument(
     49        "-iv",
     50        "--increment_version",
     51        type=str,
     52        default=None,
     53        choices=["major", "minor", "patch"],
     54        help=""" Increment version in pyproject.toml.
     55        Can be one of "major", "minor", or "patch". """,
     56    )
     57
     58    parser.add_argument(
     59        "-p",
     60        "--publish",
     61        action="store_true",
     62        help=""" Publish package to PyPi.
     63        Note: You must have configured twine
     64        and registered a PyPi account/generated an API
     65        key to use this option.""",
     66    )
     67
     68    parser.add_argument(
     69        "-rt",
     70        "--run_tests",
     71        action="store_true",
     72        help=""" Run tests for the package. """,
     73    )
     74
     75    parser.add_argument(
     76        "-gt",
     77        "--generate_tests",
     78        action="store_true",
     79        help=""" Generate tests for the package. """,
     80    )
     81
     82    parser.add_argument(
     83        "-uc",
     84        "--update_changelog",
     85        action="store_true",
     86        help=""" Update changelog file. """,
     87    )
     88
     89    parser.add_argument(
     90        "-od",
     91        "--overwrite_dependencies",
     92        action="store_true",
     93        help=""" When building a package, packagelister will be used
     94        to update the dependencies list in pyproject.toml.
     95        The default behavior is to append any new dependencies to
     96        the current list so as not to erase any manually added dependencies
     97        that packagelister may not detect. If you don't have any manually
     98        added dependencies and want to remove any dependencies that your
     99        project no longer uses, pass this flag.""",
    100    )
    101
    102    parser.add_argument(
    103        "-ca",
    104        "--commit_all",
    105        type=str,
    106        default=None,
    107        help=""" Git stage and commit all tracked files with this supplied commit message.
    108        If 'build' is passed, all commits will have message: 'chore: build v{current_version}""",
    109    )
    110
    111    parser.add_argument(
    112        "-s",
    113        "--sync",
    114        action="store_true",
    115        help=""" Pull from github, then push current commit to repo. """,
    116    )
    117
    118    parser.add_argument(
    119        "-dv",
    120        "--dependency_versions",
    121        action="store_true",
    122        help=""" Include version specifiers for dependencies in
    123        pyproject.toml.""",
    124    )
    125
    126    parser.add_argument(
    127        "-up",
    128        "--update",
    129        type=str,
    130        default=None,
    131        choices=["major", "minor", "patch"],
    132        help=""" Expects one argument: "major", "minor", or "patch".
    133        Passing "-up minor" is equivalent to passing "--build --tag_version --increment_version minor --update_changelog --commit_all build --sync".
    134        To publish the updated package, the -p/--publish switch needs to be added to the cli input.
    135        To install the updated package, the -i/--install switch also needs to be added.""",
    136    )
    137
    138    parser.add_argument(
    139        "-st",
    140        "--skip_tests",
    141        action="store_true",
    142        help=""" Don't run tests when using the -b/--build command. """,
    143    )
    144
    145    parser.add_argument(
    146        "-ip",
    147        "--is_published",
    148        action="store_true",
    149        help=""" Check that the version number in `pyproject.toml` and `pypi.org/project/{project_name}` agree. """,
    150    )
    151
    152    args = parser.parse_args()
    153
    154    args.package = Pathier(args.package).resolve()
    155
    156    if args.update:
    157        args.build = True
    158        args.tag_version = True
    159        args.increment_version = args.update
    160        args.update_changelog = True
    161        args.commit_all = "build"
    162        args.sync = True
    163
    164    if args.increment_version and args.increment_version not in [
    165        "major",
    166        "minor",
    167        "patch",
    168    ]:
    169        raise ValueError(
    170            f"Invalid option for -iv/--increment_version: {args.increment_version}"
    171        )
    172
    173    if args.commit_all == "":
    174        raise ValueError("Commit message for args.commit_all cannot be empty.")
    175
    176    if args.publish and not hassle_utilities.on_primary_branch():
    177        print(
    178            "WARNING: You are trying to publish a project that does not appear to be on its main branch."
    179        )
    180        choice = input("Continue? (y/n) ")
    181        if choice != "y":
    182            print("Quitting hassle.")
    183            sys.exit()
    184
    185    return args
    186
    187
    188def build(
    189    package_dir: Pathier,
    190    skip_tests: bool = False,
    191    overwrite_dependencies: bool = False,
    192    increment_version: str | None = None,
    193):
    194    """Perform the build process.
    195
    196    Steps:
    197    * Run tests (unless `skip_tests` is `True`)
    198    * Raise error and abandon build if tests fail
    199    * Format source code with `Black`
    200    * Sort source code imports with `isort`
    201    * Update project dependencies in `pyproject.toml`
    202    * Increment version in `pyproject.toml` if `increment_version` supplied
    203    * Generate docs
    204    * Delete previous `dist` folder contents
    205    * Invoke build module"""
    206    if not skip_tests and not run_tests(package_dir):
    207        raise RuntimeError(
    208            f"ERROR: {package_dir.stem} failed testing.\nAbandoning build."
    209        )
    210    hassle_utilities.format_files(package_dir)
    211    [isort.file(path) for path in package_dir.rglob("*.py")]
    212    hassle_utilities.update_dependencies(
    213        package_dir / "pyproject.toml", overwrite_dependencies
    214    )
    215    if increment_version:
    216        hassle_utilities.increment_version(
    217            package_dir / "pyproject.toml", increment_version
    218        )
    219    # Vermin isn't taking into account the minimum version of dependencies.
    220    # Removing from now and defaulting to >=3.10
    221    # hassle_utilities.update_minimum_python_version(pyproject_path)
    222    hassle_utilities.generate_docs(package_dir)
    223    (package_dir / "dist").delete()
    224    os.system(f"{sys.executable} -m build {package_dir}")
    225
    226
    227def main(args: argparse.Namespace = None):
    228    if not args:
    229        args = get_args()
    230
    231    pyproject_path = args.package / "pyproject.toml"
    232    args.package.mkcwd()
    233
    234    git = Git()
    235
    236    if not pyproject_path.exists():
    237        raise FileNotFoundError(f"Could not locate pyproject.toml for {args.package}")
    238
    239    if args.generate_tests:
    240        generate_test_files(args.package)
    241
    242    if args.run_tests:
    243        run_tests(args.package)
    244
    245    if args.build:
    246        build(
    247            args.package,
    248            args.skip_tests,
    249            args.overwrite_dependencies,
    250            args.increment_version,
    251        )
    252
    253    if args.increment_version and not args.build:
    254        hassle_utilities.increment_version(pyproject_path, args.increment_version)
    255
    256    if args.commit_all:
    257        if args.commit_all == "build":
    258            version = pyproject_path.loads()["project"]["version"]
    259            args.commit_all = f"chore: build v{version}"
    260        git.add_all()
    261        git.commit(f'-m "{args.commit_all}"')
    262
    263    if args.tag_version:
    264        hassle_utilities.tag_version(args.package)
    265
    266    if args.update_changelog:
    267        hassle_utilities.update_changelog(pyproject_path)
    268        if args.tag_version:
    269            with git.capturing_output():
    270                tags = git.tag("--sort=-committerdate").stdout
    271                most_recent_tag = tags[: tags.find("\n")]
    272                git.tag(f"-d {most_recent_tag}")
    273        input("Press enter to continue after manually adjusting the changelog...")
    274        git.commit_files(
    275            [str(args.package / "CHANGELOG.md")], "chore: update changelog"
    276        )
    277        if args.tag_version:
    278            with git.capturing_output():
    279                git.tag(most_recent_tag)
    280
    281    if args.publish:
    282        os.system(f"twine upload {args.package / 'dist' / '*'}")
    283
    284    if args.install:
    285        os.system(
    286            f"{sys.executable} -m pip install {args.package} --no-deps --upgrade --no-cache-dir"
    287        )
    288
    289    if args.sync:
    290        git.pull(f"origin {git.current_branch} --tags")
    291        git.push(f"origin {git.current_branch} --tags")
    292
    293    if args.is_published:
    294        is_published = hassle_utilities.latest_version_is_published(
    295            args.package / "pyproject.toml"
    296        )
    297        if is_published:
    298            print("The most recent version of this package has been published.")
    299        else:
    300            print("The most recent version of this package has not been published.")
    301
    302
    303if __name__ == "__main__":
    304    main(get_args())

def get_args() -> argparse.Namespace: View Source

     17def get_args() -> argparse.Namespace:
     18    parser = argparse.ArgumentParser()
     19
     20    parser.add_argument(
     21        "package",
     22        type=str,
     23        default=".",
     24        nargs="?",
     25        help=""" The name of the package or project to use,
     26        assuming it's a subfolder of your current working directory.
     27        Can also be a full path to the package. If nothing is given,
     28        the current working directory will be used.""",
     29    )
     30
     31    parser.add_argument(
     32        "-b", "--build", action="store_true", help=""" Build the package. """
     33    )
     34
     35    parser.add_argument(
     36        "-t",
     37        "--tag_version",
     38        action="store_true",
     39        help=""" Add a git tag corresponding to the version in pyproject.toml. """,
     40    )
     41
     42    parser.add_argument(
     43        "-i",
     44        "--install",
     45        action="store_true",
     46        help=""" Install the package from source. """,
     47    )
     48
     49    parser.add_argument(
     50        "-iv",
     51        "--increment_version",
     52        type=str,
     53        default=None,
     54        choices=["major", "minor", "patch"],
     55        help=""" Increment version in pyproject.toml.
     56        Can be one of "major", "minor", or "patch". """,
     57    )
     58
     59    parser.add_argument(
     60        "-p",
     61        "--publish",
     62        action="store_true",
     63        help=""" Publish package to PyPi.
     64        Note: You must have configured twine
     65        and registered a PyPi account/generated an API
     66        key to use this option.""",
     67    )
     68
     69    parser.add_argument(
     70        "-rt",
     71        "--run_tests",
     72        action="store_true",
     73        help=""" Run tests for the package. """,
     74    )
     75
     76    parser.add_argument(
     77        "-gt",
     78        "--generate_tests",
     79        action="store_true",
     80        help=""" Generate tests for the package. """,
     81    )
     82
     83    parser.add_argument(
     84        "-uc",
     85        "--update_changelog",
     86        action="store_true",
     87        help=""" Update changelog file. """,
     88    )
     89
     90    parser.add_argument(
     91        "-od",
     92        "--overwrite_dependencies",
     93        action="store_true",
     94        help=""" When building a package, packagelister will be used
     95        to update the dependencies list in pyproject.toml.
     96        The default behavior is to append any new dependencies to
     97        the current list so as not to erase any manually added dependencies
     98        that packagelister may not detect. If you don't have any manually
     99        added dependencies and want to remove any dependencies that your
    100        project no longer uses, pass this flag.""",
    101    )
    102
    103    parser.add_argument(
    104        "-ca",
    105        "--commit_all",
    106        type=str,
    107        default=None,
    108        help=""" Git stage and commit all tracked files with this supplied commit message.
    109        If 'build' is passed, all commits will have message: 'chore: build v{current_version}""",
    110    )
    111
    112    parser.add_argument(
    113        "-s",
    114        "--sync",
    115        action="store_true",
    116        help=""" Pull from github, then push current commit to repo. """,
    117    )
    118
    119    parser.add_argument(
    120        "-dv",
    121        "--dependency_versions",
    122        action="store_true",
    123        help=""" Include version specifiers for dependencies in
    124        pyproject.toml.""",
    125    )
    126
    127    parser.add_argument(
    128        "-up",
    129        "--update",
    130        type=str,
    131        default=None,
    132        choices=["major", "minor", "patch"],
    133        help=""" Expects one argument: "major", "minor", or "patch".
    134        Passing "-up minor" is equivalent to passing "--build --tag_version --increment_version minor --update_changelog --commit_all build --sync".
    135        To publish the updated package, the -p/--publish switch needs to be added to the cli input.
    136        To install the updated package, the -i/--install switch also needs to be added.""",
    137    )
    138
    139    parser.add_argument(
    140        "-st",
    141        "--skip_tests",
    142        action="store_true",
    143        help=""" Don't run tests when using the -b/--build command. """,
    144    )
    145
    146    parser.add_argument(
    147        "-ip",
    148        "--is_published",
    149        action="store_true",
    150        help=""" Check that the version number in `pyproject.toml` and `pypi.org/project/{project_name}` agree. """,
    151    )
    152
    153    args = parser.parse_args()
    154
    155    args.package = Pathier(args.package).resolve()
    156
    157    if args.update:
    158        args.build = True
    159        args.tag_version = True
    160        args.increment_version = args.update
    161        args.update_changelog = True
    162        args.commit_all = "build"
    163        args.sync = True
    164
    165    if args.increment_version and args.increment_version not in [
    166        "major",
    167        "minor",
    168        "patch",
    169    ]:
    170        raise ValueError(
    171            f"Invalid option for -iv/--increment_version: {args.increment_version}"
    172        )
    173
    174    if args.commit_all == "":
    175        raise ValueError("Commit message for args.commit_all cannot be empty.")
    176
    177    if args.publish and not hassle_utilities.on_primary_branch():
    178        print(
    179            "WARNING: You are trying to publish a project that does not appear to be on its main branch."
    180        )
    181        choice = input("Continue? (y/n) ")
    182        if choice != "y":
    183            print("Quitting hassle.")
    184            sys.exit()
    185
    186    return args

def build( package_dir: pathier.pathier.Pathier, skip_tests: bool = False, overwrite_dependencies: bool = False, increment_version: str | None = None): View Source

    189def build(
    190    package_dir: Pathier,
    191    skip_tests: bool = False,
    192    overwrite_dependencies: bool = False,
    193    increment_version: str | None = None,
    194):
    195    """Perform the build process.
    196
    197    Steps:
    198    * Run tests (unless `skip_tests` is `True`)
    199    * Raise error and abandon build if tests fail
    200    * Format source code with `Black`
    201    * Sort source code imports with `isort`
    202    * Update project dependencies in `pyproject.toml`
    203    * Increment version in `pyproject.toml` if `increment_version` supplied
    204    * Generate docs
    205    * Delete previous `dist` folder contents
    206    * Invoke build module"""
    207    if not skip_tests and not run_tests(package_dir):
    208        raise RuntimeError(
    209            f"ERROR: {package_dir.stem} failed testing.\nAbandoning build."
    210        )
    211    hassle_utilities.format_files(package_dir)
    212    [isort.file(path) for path in package_dir.rglob("*.py")]
    213    hassle_utilities.update_dependencies(
    214        package_dir / "pyproject.toml", overwrite_dependencies
    215    )
    216    if increment_version:
    217        hassle_utilities.increment_version(
    218            package_dir / "pyproject.toml", increment_version
    219        )
    220    # Vermin isn't taking into account the minimum version of dependencies.
    221    # Removing from now and defaulting to >=3.10
    222    # hassle_utilities.update_minimum_python_version(pyproject_path)
    223    hassle_utilities.generate_docs(package_dir)
    224    (package_dir / "dist").delete()
    225    os.system(f"{sys.executable} -m build {package_dir}")

Perform the build process.

Steps:

- Run tests (unless `skip_tests` is `True`)
- Raise error and abandon build if tests fail
- Format source code with `Black`
- Sort source code imports with `isort`
- Update project dependencies in `pyproject.toml`
- Increment version in `pyproject.toml` if `increment_version` supplied
- Generate docs
- Delete previous `dist` folder contents
- Invoke build module

def main(args: argparse.Namespace = None): View Source

    228def main(args: argparse.Namespace = None):
    229    if not args:
    230        args = get_args()
    231
    232    pyproject_path = args.package / "pyproject.toml"
    233    args.package.mkcwd()
    234
    235    git = Git()
    236
    237    if not pyproject_path.exists():
    238        raise FileNotFoundError(f"Could not locate pyproject.toml for {args.package}")
    239
    240    if args.generate_tests:
    241        generate_test_files(args.package)
    242
    243    if args.run_tests:
    244        run_tests(args.package)
    245
    246    if args.build:
    247        build(
    248            args.package,
    249            args.skip_tests,
    250            args.overwrite_dependencies,
    251            args.increment_version,
    252        )
    253
    254    if args.increment_version and not args.build:
    255        hassle_utilities.increment_version(pyproject_path, args.increment_version)
    256
    257    if args.commit_all:
    258        if args.commit_all == "build":
    259            version = pyproject_path.loads()["project"]["version"]
    260            args.commit_all = f"chore: build v{version}"
    261        git.add_all()
    262        git.commit(f'-m "{args.commit_all}"')
    263
    264    if args.tag_version:
    265        hassle_utilities.tag_version(args.package)
    266
    267    if args.update_changelog:
    268        hassle_utilities.update_changelog(pyproject_path)
    269        if args.tag_version:
    270            with git.capturing_output():
    271                tags = git.tag("--sort=-committerdate").stdout
    272                most_recent_tag = tags[: tags.find("\n")]
    273                git.tag(f"-d {most_recent_tag}")
    274        input("Press enter to continue after manually adjusting the changelog...")
    275        git.commit_files(
    276            [str(args.package / "CHANGELOG.md")], "chore: update changelog"
    277        )
    278        if args.tag_version:
    279            with git.capturing_output():
    280                git.tag(most_recent_tag)
    281
    282    if args.publish:
    283        os.system(f"twine upload {args.package / 'dist' / '*'}")
    284
    285    if args.install:
    286        os.system(
    287            f"{sys.executable} -m pip install {args.package} --no-deps --upgrade --no-cache-dir"
    288        )
    289
    290    if args.sync:
    291        git.pull(f"origin {git.current_branch} --tags")
    292        git.push(f"origin {git.current_branch} --tags")
    293
    294    if args.is_published:
    295        is_published = hassle_utilities.latest_version_is_published(
    296            args.package / "pyproject.toml"
    297        )
    298        if is_published:
    299            print("The most recent version of this package has been published.")
    300        else:
    301            print("The most recent version of this package has not been published.")