Skip to main content

Create, build, test, and publish Python projects and packages.

Project description

Hassle

Automate creating, building, testing, and publishing Python projects and packages from the command line.
This package wraps several other packages and tools to streamline the package creation and distribution workflow for smaller scale personal projects.

Installation

Install with:

pip install hassle

Additional setup:

Install git and add it to your PATH if it already isn't.
You will also need to register a pypi account if you want to publish pip-installable packages to https://pypi.org with this tool.
Once you've created and validated an account, you will need to follow the directions to generate an api key.
Copy the key and in your home directory, create a '.pypirc' file if it doesn't already exist.
Edit the file so it contains the following (don't include the brackets around your api key):

[distutils]
index-servers =
    pypi

[pypi]
username = __token__
password = pypi-{The api key you copied}

Configuration

After installation and the above additional setup, it is a good idea to run the 'hassle_config' tool. This isn't required and a blank config will be generated whenever another tool needs it if it doesn't exist. This info, if provided, is used to populate a new project's 'pyproject.toml' file. Invoking the tool with the -h switch shows the following:

C:\python>hassle_config -h
usage: hassle_config [-h] [-n NAME] [-e EMAIL] [-g GITHUB_USERNAME] [-d DOCS_URL] [-t TAG_PREFIX]

options:
  -h, --help            show this help message and exit
  -n NAME, --name NAME  Your name. This will be used to populate the 'authors' field of a packages 'pyproject.toml'.
  -e EMAIL, --email EMAIL
                        Your email. This will be used to populate the 'authors' field of a packages 'pyproject.toml'.
  -g GITHUB_USERNAME, --github_username GITHUB_USERNAME
                        Your github account name. When creating a new package, say with the name 'mypackage', the pyproject.toml 'Homepage' field will be set to 'https://github.com/{github_username}/mypackage' and the 'Source code' field will be set to
                        'https://github.com/{github_username}/mypackage/tree/main/src/mypackage'.
  -d DOCS_URL, --docs_url DOCS_URL
                        The template url to be used in your pyproject.toml file indicating where your project docs will be hosted. Pass the url such that the spot the actual package name will go is held by '$name', e.g. 'https://somedocswebsite/user/projects/$name'. If
                        'hassle_config.toml' didn't exist prior to running this tool and nothing is given for this arg, it will default to using the package's github url. e.g. for package 'mypackage' the url will be 'https://github.com/{your_github_name}/mypackage/tree/main/docs'
  -t TAG_PREFIX, --tag_prefix TAG_PREFIX
                        The tag prefix to use with git when tagging source code versions. e.g. hassle will use the current version in your pyproject.toml file to when adding a git tag. If you've passed 'v' to this arg and the version of your hypothetical package is '1.0.1', it will
                        be tagged as 'v1.0.1'. If 'hassle_config.toml' didn't exist prior to running this tool and you don't pass anything for this arg, it will default to ''.

Invoking 'hassle_config' with no arguments will create a blank config if a config file doesn't already exist and it will also print where the config file is located so manual edits can be made.
On a typical Python installation that'll look something like:

C:\python>hassle_config
Manual edits can be made at C:\Users\%USER%\AppData\Local\Programs\Python\Python311\Lib\site-packages\hassle\hassle_config.toml

Generating New Projects

New projects are generated by invoking the "new_project" tool from your terminal.
The -h/--help switch produces the following:

C:\python>new_project -h
usage: new_project [-h] [-s [SOURCE_FILES ...]] [-d DESCRIPTION] [-dp [DEPENDENCIES ...]] [-k [KEYWORDS ...]] [-as] [-nl] [-os [OPERATING_SYSTEM ...]] [-np] name

positional arguments:
  name                  Name of the package to create in the current working directory.

options:
  -h, --help            show this help message and exit
  -s [SOURCE_FILES ...], --source_files [SOURCE_FILES ...]
                        List of additional source files to create in addition to the default __init__.py and {name}.py files.
  -d DESCRIPTION, --description DESCRIPTION
                        The package description to be added to the pyproject.toml file.
  -dp [DEPENDENCIES ...], --dependencies [DEPENDENCIES ...]
                        List of dependencies to add to pyproject.toml. Note: hassle.py will automatically scan your project for 3rd party imports and update pyproject.toml. This switch is largely useful for adding dependencies your project might need, but doesn't directly import
                        in any source files, like an os.system() call that invokes a 3rd party cli.
  -k [KEYWORDS ...], --keywords [KEYWORDS ...]
                        List of keywords to be added to the keywords field in pyproject.toml.
  -as, --add_script     Add section to pyproject.toml declaring the package should be installed with command line scripts added. The default is '{name} = "{name}.{name}:main". You will need to manually change this field.
  -nl, --no_license     By default, projects are created with an MIT license. Set this flag to avoid adding a license if you want to configure licensing at another time.
  -os [OPERATING_SYSTEM ...], --operating_system [OPERATING_SYSTEM ...]
                        List of operating systems this package will be compatible with. The default is OS Independent. This only affects the 'classifiers' field of pyproject.toml .
  -np, --not_package    Put source files in top level directory and delete tests folder.

Most of these options pertain to prefilling the generated 'pyproject.toml' file.
As a simple example we'll create a new package called 'nyquil' with the following:

C:\python>new_project nyquil -d "A package to help you sleep when you're sick." -k "sleep" "sick"

You should see the following output:

reformatted C:\python\nyquil\tests\test_nyquil.py

All done! ✨ 🍰 ✨
1 file reformatted.
Fixing C:\python\nyquil\tests\test_nyquil.py
Initialized empty Git repository in C:/python/nyquil/.git/

A new folder in your current working directory called 'nyquil' should now exist.
It should have the following structure:

nyquil
|  |-.git
|  |  |-config
|  |  |-description
|  |  |-HEAD
|  |  |-hooks
|  |  |  |-applypatch-msg.sample
|  |  |  |-commit-msg.sample
|  |  |  |-fsmonitor-watchman.sample
|  |  |  |-post-update.sample
|  |  |  |-pre-applypatch.sample
|  |  |  |-pre-commit.sample
|  |  |  |-pre-merge-commit.sample
|  |  |  |-pre-push.sample
|  |  |  |-pre-rebase.sample
|  |  |  |-pre-receive.sample
|  |  |  |-prepare-commit-msg.sample
|  |  |  |-push-to-checkout.sample
|  |  |  |_update.sample
|  |  |
|  |  |-info
|  |  |  |_exclude
|  |
|  |-.gitignore
|  |-.vscode
|  |  |_settings.json
|  |
|  |-LICENSE.txt
|  |-pyproject.toml
|  |-README.md
|  |-src
|  |  |-nyquil
|  |  |  |-__init__.py
|  |  |  |_nyquil.py
|  |
|  |-tests
|  |  |_test_nyquil.py

'new_project' has generated our project structure and files for us as well as initialized a git repository.
Note: By default 'new_project' adds an MIT License to the project. Pass the -nl/--no_license flag to prevent this behavior.
If you open the 'pyproject.toml' file it should look like the following except for the 'project.authors' and 'project.urls' sections:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "nyquil"
description = "A package to help you sleep when you're sick."
version = "0.0.0"
requires-python = "3.0"
dependencies = []
readme = "README.md"
keywords = ["sleep", "sick"]
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    ]

[[project.authors]]
name = "Matt Manes"
email = "mattmanes@pm.me"

[project.urls]
"Homepage" = "https://github.com/matt-manes/nyquil"
"Documentation" = "https://github.com/matt-manes/nyquil/tree/main/docs"
"Source code" = "https://github.com/matt-manes/nyquil/tree/main/src/nyquil"

[tool.pytest.ini_options]
addopts = [
    "--import-mode=importlib",
    ]
pythonpath = "src"

[tool.hatch.build.targets.sdist]
exclude = [
    ".coverage",
    ".pytest_cache",
    ".vscode",
    "tests",
    ".gitignore"
    ]
[project.scripts]

The package would do absolutely nothing, but with the generated files we do have the viable minimum to build an installable python package.

Generating Tests

While Hassle won't write your tests for you, it will generate the scaffolding to write tests for you.
When you run the tool, it will scan the files in your 'src' directory and generate placeholders for each function in the file and place them in a test file in the 'tests' directory.
If the test file already exists, functions will not be duplicated and existing content will not be overwritten, only appended to.

Let's navigate into our new 'nyquil' folder from the terminal:

C:\python>cd nyquil

Note: All of the following Hassle tools can be run from the parent folder of 'nyquil', but you will need to specify the package name as the first argument, since we're navigating into 'nyquil' we can omit it.

Before we run the test generator tool, we need to add something to test.
Open up the 'nyquil.py' file in the 'src' directory and add the following (be sure to save):

from pathlib import Path

import tomlkit

root = Path(__file__).parent


def get_project_name() -> str:
    """Return the name of this project from its pyproject.toml file."""
    content = tomlkit.loads((root.parent.parent / "pyproject.toml").read_text())
    return content["project"]["name"]

There are two ways to generate tests:

C:\python\nyquil>generate_tests

and

C:\python\nyquil>hassle -gt

They both produce the same results, so we'll use the shorter one.
After running hassle -gt in your terminal, look at the 'test_nyquil.py' file inside the 'tests' folder.
It should look like this:

import pytest

from nyquil import nyquil


def test__nyquil__get_project_name():
    ...

Go ahead and modify it to this (and save):

import pytest

from nyquil import nyquil


def test__nyquil__get_project_name():
    assert "nyquil" == nyquil.get_project_name()

Running Tests

Similarly to generating tests, running tests can be done with either

C:\python\nyquil>run_tests

or

C:\python\nyquil>hassle -rt

and as before we'll stick with the first.

Hassle uses Pytest and Coverage to run tests, so when we invoke the hassle -rt command, we should see something like this:

C:\python\nyquil>hassle -rt
================================================================================================================================== test session starts ==================================================================================================================================
platform win32 -- Python 3.11.0, pytest-7.2.1, pluggy-1.0.0
rootdir: C:\python\nyquil, configfile: pyproject.toml
plugins: anyio-3.6.2, hypothesis-6.63.0
collected 1 item

tests\test_nyquil.py .

=================================================================================================================================== 1 passed in 0.06s ===================================================================================================================================
Name                     Stmts   Miss  Cover   Missing
------------------------------------------------------
src\nyquil\__init__.py       0      0   100%
src\nyquil\nyquil.py         6      0   100%
tests\test_nyquil.py         4      0   100%
------------------------------------------------------
TOTAL                       10      0   100%

For more about testing refer to the Pytest documentation.

Building

Building the package is as simple as running the following:

C:\python\nyquil>hassle -b

which should produce

All done! ✨ 🍰 ✨
3 files left unchanged.
Skipped 1 files
 [____________________________________________________________________________________________]-100.00% Scanning test_nyquil.py
* Creating venv isolated environment...
* Installing packages in isolated environment... (hatchling)
* Getting build dependencies for sdist...
* Building sdist...
* Building wheel from sdist
* Creating venv isolated environment...
* Installing packages in isolated environment... (hatchling)
* Getting build dependencies for wheel...
* Building wheel...
Successfully built nyquil-0.0.0.tar.gz and nyquil-0.0.0-py3-none-any.whl

There should be two new folders in the top nyquil directory: "dist" and "docs".
The "dist" folder contains the tar.gz and .whl files that are needed to install the package and "docs" contains .html and .js files autogenerated by the pdoc package.

The build command also invokes the Vermin package to determine the minimum Python version our package will support as well as the Packagelister package to determine our package's dependencies.
hassle then updates the 'pyproject.toml' file with this new information.
The version of pyproject that was generated initially showed this for "requires-python" and "dependencies":

requires-python = "3.0"
dependencies = []

but now it should show

requires-python = ">=3.4"
dependencies = ["tomlkit~=0.11.6", "pytest~=7.2.1"]

Another command line switch for Hassle that's relevant to this is the -od/--overwrite_dependencies flag.
The defualt behavior of hassle -b is to append any new packages it finds to the dependencies list that aren't already there.
This is good for when you've manually added dependencies your package needs that aren't explicitly imported in any of your source files.
For instance, Hassle invokes a lot of things through the os.system() function, such as the Black and Isort packages.
Packagelister doesn't pick these up even though Hassle depends on them because they're never explicitly imported into a source file.

However if our package doesn't have any manually added dependencies like that and, after some modifications from our first build, we know there are some packages we were using that we don't use anymore, we can run

C:\python\nyquil>hassle -b -od

and our dependencies list will get overwritten, effectively removing the packages we no longer need in our project.

Installing

We can install the package to our "site-packages" directory like any other package so that it's available to import with the command

C:\python\nyquil>hassle -i

Publishing

Assuming you've set up a PyPi account, generated the api key, and configured the '.pypirc' file as mentioned earlier, then you can publish the current version of your package by running

C:\python\nyquil>hassle -p

Updating

After fixing some bugs or adding some features, you can increment your version number in the pyproject file with the -iv/--increment_version flag using one of three arguments: major, minor, or patch. This follows the semantic versioning standard so, if the project's current version is 1.3.7, then

>hassle -iv patch

produces 1.3.8,

>hassle -iv minor

produces 1.4.0,

and

>hassle -iv major

produces 2.0.0

Git Stuff

Version Tagging

The command >hassle -t can be used to git tag your repo's current state according to the version number in the pyproject file and the tag prefix in your hassle_config file we made earlier.

Committing

Using the -ca/--commit_all flag followed by a commit message with Hassle will git commit all uncommitted files with the provided message.
If you only pass "build" as the message, i.e. >hassle -ca build, all uncommitted files will be committed with the message chore: build v<pyproject.toml_version>.
This is particulary useful to run with the build flag so as to commit the files created and modified by the build process such as "dist", "docs", etc.

Syncing

**Note: This section requires Git and GitHub to be able to talk to each other.
If you need help with that, look here.

The -s/--sync flag can be used to sync your changes with your remote repo.
Running

>hassle -s

really just invokes

>git pull --tags origin main
>git push origin main:main --tags

So for our example package 'nyquil', we can log into github and create an empty repository named "nyquil".
Then, in your terminal, run the command

C:\python\nyquil> git remote add origin https://github.com/{your-username}/nyquil.git

Now you should be able to sync your local commits to GitHub using >hassle -s.

Changelog Generation

You can also generate a formatted changelog using >hassle -uc.
This isn't strictly a Git operation, but it invokes the auto-changelog package and relies on git version tagging, conventional commit style commit messages, and your remote repo.

Tying It All Together

Let's fastfoward in time and assume we've already published our 'nyquil' package as version 0.0.0.
We've decided to add some non-breaking additions to our package.
Assuming we've dilligently done our tests and updated our readme, we can accomplish the whole build, publish, and sync process with the command

C:\python\nyquil>hassle -b -t -i -iv minor -p -uc -ca build -s

This will:

  • increment our pyproject version to 0.1.0
  • delete the previous distribution files
  • update our package's dependencies
  • update our package's minimum Python version
  • generate updated documentation
  • build the distributable 'tar.gz' and '.wheel' files
  • update our changelog and git commit it with the message chore: update changelog
  • git commit the rest of the modifications and additions with the message chore: build v0.1.0
  • git tag the current code state as <prefix_in_hassle_config_.toml>0.1.0
  • publish the updated package to PyPi
  • install the package to our system
  • sync everything to our remote repo.

For reference, here is the full -h/--help output for hassle:

>hassle -h
usage: hassle [-h] [-b] [-t] [-i] [-iv {major,minor,patch}] [-p] [-rt] [-gt] [-uc] [-od] [-ca COMMIT_ALL] [-s] [-dv] [-up {major,minor,patch}] [-st] [-ip] [package]

positional arguments:
  package               The name of the package or project to use, assuming it's a subfolder of your current working directory. Can also be a full path to the package. If nothing is given, the current working directory will be used.

options:
  -h, --help            show this help message and exit
  -b, --build           Build the package.
  -t, --tag_version     Add a git tag corresponding to the version in pyproject.toml.
  -i, --install         Install the package from source.
  -iv {major,minor,patch}, --increment_version {major,minor,patch}
                        Increment version in pyproject.toml. Can be one of "major", "minor", or "patch".
  -p, --publish         Publish package to PyPi. Note: You must have configured twine and registered a PyPi account/generated an API key to use this option.
  -rt, --run_tests      Run tests for the package.
  -gt, --generate_tests
                        Generate tests for the package.
  -uc, --update_changelog
                        Update changelog file.
  -od, --overwrite_dependencies
                        When building a package, packagelister will be used to update the dependencies list in pyproject.toml. The default behavior is to append any new dependencies to the current list so as not to erase any manually added dependencies that packagelister may not
                        detect. If you don't have any manually added dependencies and want to remove any dependencies that your project no longer uses, pass this flag.
  -ca COMMIT_ALL, --commit_all COMMIT_ALL
                        Git stage and commit all tracked files with this supplied commit message. If 'build' is passed, all commits will have message: 'chore: build v{current_version}
  -s, --sync            Pull from github, then push current commit to repo.
  -dv, --dependency_versions
                        Include version specifiers for dependencies in pyproject.toml.
  -up {major,minor,patch}, --update {major,minor,patch}
                        Expects one argument: "major", "minor", or "patch". Passing "-up minor" is equivalent to passing "--build --tag_version --increment_version minor --update_changelog --commit_all build --sync". To publish the updated package, the -p/--publish switch needs
                        to be added to the cli input. To install the updated package, the -i/--install switch also needs to be added.
  -st, --skip_tests     Don't run tests when using the -b/--build command.
  -ip, --is_published   Check that the version number in `pyproject.toml` and `pypi.org/project/{project_name}` agree.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hassle-2.12.4.tar.gz (160.0 kB view hashes)

Uploaded Source

Built Distribution

hassle-2.12.4-py3-none-any.whl (24.0 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page