(WIP: some of the described features here might not have been implemented. Currently only linux is supported (espacially tested. This might change in the future)
Lets start with some code and a maybe-timeline of a package-development-process.
-
Have an idea and an name -> develop horst2
-
make a folder called horst2 and inside start a package horst2: -> horst2 | - horst2 | - init.py
-
make your first build.py
from horst import *
Horst(__file__)
- open a commandline
horst2 $ horst
>>>
Options:
-d, --dry DryRun nothing will be executed
-v, --verbose Output everything to cli
--help Show this message and exit.
Commands:
env:create
env:update
test
debug
- Create a new virutalenv by:
$ horst env:create
Now you have a new virtualenv in your main-folder called ".env"
- write some code and tests
- run all test:
horst test
- devlop some more code and build your package as a wheel
horst build
this command will run first your tests and then build your package as wheel
- release your build
horst release
this command will: - run a build (with your tests in front) - copy your tests to a new temporary directory. - make a new virtualenv with only the test-dependencies - install your package in environment - run all tests in this environment
This should check if your forgot to specify a dependency or to include some data as package_data
horst is a build-automation-system. It aims to help python-developers to get an more easy experience when it comes to building a package and releasing it. For that horst tries to be as simple as possible and does not invent anything new. Because of this horst depends on many great libraries like: - pytest + plugins - bumpversion - flake8 - virtualenv - ...
Horst is extensible and can be used as a task-runner, BUT there are better projects out there, which mostly focus on this part and do this job with a smaller dependency-footprint. Said this, the biggest difference between HORST and others, is that it tries to come with batteries-fully-included.
-
small footprint: you NEVER want to have Horst as a production dependency! There are to many downstream dependencies from Horst
-
a new build-system: unlike conda or others, the main goal of horst is to provide a good "standard" - process for developing and releasing packages. Horst simply orchestreates other "standard" - tools
-
The one true way to do it: At the moment Horst simply refelects what I believe is a good process. You may not agree with me, thats fine. Horst tries to rely on as many "pythonic" - conventions as possible, but it is opinionated. There are ways to change Horst behaviour, but sometimes it may be easier to stick to the Horst-way
The build.py describes your package / process. This file should be located right next to a potential setup.py (although the setup.py will be generated by horst). Invoking the horst commandline, horst will look for a build.py file and evaluate it. You can use arbitrary python code in this file. A more advanced build.py file looks like this:
from horst import *
Horst(__file__)
Horst(__file__)
dependencies(
install=["click", "jinja2", "bumpversion", "pytest-cov", "pytest", "virtualenv"],
test=[],
build=[],
environment=virtualenv(
{
".env": {'python': "python3", 'main': True},
}
)
)
package(
name="horst",
version=bumpversion(),
description="Horst is a simple build-automation-tool for python packages",
url=from_git_config("origin"),
)
test(
unittest=pytest(
exclude=[marked_as("slow")],
include=[],
report=junit(path=path.join(".testresults", "results.xml"), prefix=""),
coverage=pytest_coverage(
report=["html", "term"],
)
),
this will give you a complete and custom setup for building and deploying your package
all functions should configure a effect / task on a stage. all registered conf-function will be executed during start-up (everytime)
A configuration must be bound to a stage via decorator
@root.config(test)
def test_configuration(folder=None):
# configuration phase
yield folder, None
# creation phase
_creator_execute_tests_effect(folder)
the configuration - function is divided (by the yield-statement) in two phases.
during the configuration-phase the function arguments, or some side-effects
which are needed for the next phase are evaluated. This
data is then yielded back to horst as a global configuration
which can be queried by all other stages / routes. The second
return value is either None
or a description of routes this
stage should depend on (more on this later).
After the configuration is yielded, the function will return and
horst garantees that all other config-functions will have yielded
their data, which can then be queried. After the yield you should
call all registered routes which you want to configure and make
available at the cli.
A stage a container which can be composed with other Stages and can contain several effects. These effects will be run by horst duiring execution. To add some effects to a Stage use a decorator
dummy = Stage("dummy")
@root.register(dummy)
def config_function():
return []
Stages can be composed by using the /
- protocol
route = Stage("begin") / Stage("middle") / Stage("end")
@root.register(route)
def configure_last_stage_in_route():
return []
The code above would configure the last stage in the route. Horst will always execute routes. For that, horst will iterate over every stage in the route and execute all tasks / effects of this single-stage (TODO for the future: in parallel). Stages will be executed in way they are composed:
a = Stage("a")
b = Stage("b")
c = Stage("c")
@root.register(a) # not executable because a is just a stage
def print_a():
return Print("a - stage")
@root.register(a/b) # executable
def print_b():
return Print("a:b- route")
@root.register(a/b/c):
def print_c():
return Print("a:b:c - route. 'a' and 'a:b' were executed before")