Giter VIP home page Giter VIP logo

alfred-cli's Introduction

Alfred

PyPi Python CI CI-Windows Documentation Status Discord License

Alfred is an extensible automation tool designed to streamline repository operations. It allows you various commands as continuous integration, runner, build commands ...

You'll craft advanced commands harnessing the strengths of both worlds: shell and Python.

Demo

introductory video of alfred introductory video

Quick-start

You will generate commands to launch of the linter and unit tests process.

$ alfred --new pylint src/myapp
$ alfred --new pytest tests/unit
alfred
Usage: alfred [OPTIONS] COMMAND [ARGS]...

  alfred is an extensible automation tool designed to streamline repository
  operations.

Options:
  -d, --debug    display debug information like command runned and working
                 directory
  -v, --version  display the version of alfred
  --new          open a wizard to generate a new command
  -c, --check    check the command integrity
  --completion   display instructions to enable completion for your shell
  --help         Show this message and exit.

Commands:
  lint                run linter on codebase
  tests               run unit tests on codebase

Links

Related

alfred exists thanks to this 2 amazing open source projects.

License

MIT License

Copyright (c) 2021-2023 Fabien Arcellier

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

alfred-cli's People

Contributors

fabienarcellier avatar hassan12ammar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

hassan12ammar

alfred-cli's Issues

alfred.option parameters are documented

alfred.option is a pass-through that instantiates an option on click. Click offers a relevant set of arguments in the context of Alfred. It is necessary to summary the arguments in documentation of alfred. It is also essential to indicate this heritage to click.

Discarded alternative

  • alfred.option implements a subset of the optional click parameters: alfred.option implements only a subset of the parameters tested in the click CI. This model ensures that the parameters are fully supported by alfred. This solution restricts the possible options and the click bugs. I feel like the click arguments mostly matter.

fixed exception raised in alfred when using CTRL + C

Aborted!
Traceback (most recent call last):
  File "/home/fabien/.pyenv/versions/3.10.13/lib/python3.10/threading.py", line 1016, in _bootstrap_inner
    self.run()
  File "/home/fabien/.pyenv/versions/3.10.13/lib/python3.10/threading.py", line 953, in run
    self._target(*self._args, **self._kwargs)
  File "/home/fabien/.cache/pypoetry/virtualenvs/vangogh-tK4-d9JR-py3.10/lib/python3.10/site-packages/alfred/process.py", line 174, in _run_capture
    line = self.capture_stream.readline().decode('utf-8')
ValueError: PyMemoryView_FromBuffer(): info->buf must not be NULL

alfred --inspect show the complete configuration of project and subprojects in json

alfred juggles with python environments and environment variables. To be able to ensure a minimum of support, it is important to have information on the environment in which alfred is executed.

alfred --inspect

The order of the keys must be preserved in the json. The indentation is 2 spaces.

[
    {
        "project_directory": "./",
        "manifest": "./.alfred.toml",
        "name": "name",
        "description": "my description",
        "subprojects": [
            "./products/*"
        ],
        "project": {
            "command": [
                "alfred/*.py"
            ],
            "python_path_project_root": false,
            "python_path_extends": [],
            "venv": null,
            "venv_dotvenv_ignore": true
        },
        "runtime": {
            "executable": "",
            "pythonpath": [],
            "path": []
        }
    },
    {
        "project_directory": "./product/product1",
        "manifest": "./product/product1/.alfred.toml",
        "name": "name",
        "description": "my description",
        "subprojects": [
            "./products/*"
        ],
        "project": {
            "command": [
                "alfred/*.py"
            ],
            "python_path_project_root": false,
            "python_path_extends": [],
            "venv": null,
            "venv_dotvenv_ignore": true
        },
        "runtime": {
            "executable": "",
            "pythonpath": [],
            "path": []
        }
    },
]

the working directory in a command is always the root of the project

The working folder when an alfred command is launched is always the root of the project where the .alfred.toml manifest is located. The developer can change the folder without composing path from the alfred.project_directory function.

For example, if he wants the command to run in the frontend folder, he just uses os.chwd('frontend').

  • implement the function alfred.execution_directory returns the folder in which a user invoked the alfred command.
  • documenter la fonction alfred.execution_directory in Command toolkit

compiled library as sqlite3 are not available when loading inside the commands

This command throws an exception. ModuleNotFoundError: No module named 'sqlite3'

@alfred.command("test")
def test():
    import sqlite3
    print(sqlite3.sqlite_version)

Libraries like sqlite3 and optparse cannot be imported at runtime but math library can be imported

This code, however, works.

import sqlite3

@alfred.command("test")
def test():
    print(sqlite3.sqlite_version)

work around

The management of the path of the libraries is wrong in the code of alfred. Some paths are erased when invoking a command. A work around is to manually add the missing paths in the python_path_extends parameter.

[alfred.project]
python_path_extends = ["/usr/lib/python3.10", '/usr/lib/python3.10/lib-dynload']

These 2 paths are visible in the interpreter info

import sys
print(sys.path)

add a flag on command alfred.run to hide stdout from console

The stdout flag allows you to control whether the alfred.run command displays its information on a terminal.

_, stdout_content, _ = alfred.run("echo hello world", print_stdout=False)
  • add the flag print_stdout
  • add the flag print_stderr
  • document both flag in api section of documentation

alfred.run execute a text command directly

alfred.run execute a text command without prior preparation. The first element in text command should be the executable.

alfred.run("pytest test")
alfred.run("mv /var/yolo /var/yolo1", exit_on_error=False)

alfred text commands are more intuitive to implement.

The instruction is analyzed with shlek. We raise error on command that contains statement as '&', '|', '<', '>','>>','<<'

detect automatically virtual environment in .venv directory

alfred detects the presence of a virtual environment .venv at the root of a project. It uses the python of this virtual environment without prior configuration.

If an environment is specified in the manifest and a .venv virtual environment is also detected, the python of the virtual environment specified in the manifest is used. If the specified virtual environment is invalid, an exception is thrown.

A venv_dotvenv_ignore parameter in the alfred.toml manifest allows to disable the automatic detection of the virtual environment at the root. By default, this parameter is false.

pythonpath_extends on manifest will extend pythonpath

The python_path_extends option adds folders in the PYTHONPATH environment variable to make module resolution work from those folders.

[alfred.project]
pythonpath_extends = [ "tests" ]

In a python project, the fixtures package is not installed in the virtual environment. These modules are inaccessible for other modules, in unit tests for example. Adding a folder to the PYTHONPATH will cause python to use that folder in these dependency resolution attempts. from fixtures import alfred_fixture will be usable.

└── tests
    ├── acceptances
    ├── fixtures
    │   ├── ...
    │   ├── __init__.py
    │   └── alfred_fixture.py
    ├── __init__.py
    ├── integrations
    ├── __pycache__
    ├── spikes
    └── units

Behaviors

  • relative paths are transformed into absolute folder from the manifest's parent folder
  • absolute paths are directly added to the pythonpath
  • the pythonpath separator depends on the os (using os.pathsep)
  • paths are added at the end of the pythonpath

alfred.pythonpath register root directory and target directories

Adding a folder in the pythonpath variable allows you to expose packages without declaring them in the manifest.

This pattern is useful with poetry to be able to reuse the code of the package tests in this one for example.

The alfred.pythonpath decorator adds the project root. You can save specific folders here.

@alfred.command('ci', help="execute continuous integration process of alfred")
@alfred.pythonpath()
def ci():
    with alfred.env(SCREEN="display"):
        bash = alfred.sh("bash")
        alfred.run(bash, ["-c" "echo $SCREEN"])
@alfred.command('ci', help="execute continuous integration process of alfred")
@alfred.pythonpath(['tests'], append_root=False)
def ci():
    with alfred.env(SCREEN="display"):
        bash = alfred.sh("bash")
        alfred.run(bash, ["-c", "echo $SCREEN"])

redesign of the .alfred.yml manifest into .alfred.toml to improve the handling of alfred-cli

The .alfred.yml file is a manifest present at the root of an alfred project. It's obsolte now. It describes the project settings. The configuration of tools in python is evolving and more and more tools offer integration with the pyproject.toml file.

The first step for alfred is to migrate the .alfred.yml manifest to the toml format. At the same time, it is about making its structure more relevant. The previous manifest was structured for example to overcome the limitations of pipenv in the case of a monorepo.

Here is the new format of the .alfred.toml file.

[alfred]
name = "fixtup" #optional
description = "str" #optional
subprojects = [ "product/*", "lib/*" ] #optional

[alfred.project]
command = [ "alfred/*.py" ]
env = { }
python_path_project_root = true
python_path_extends = []
venv = "src/.."
  • expressions like alfred/*.py supports recursive wildcards like `alfred/**/*.py
  • a warning message should be raised if alfred encounters an old-format manifest .alfred.yml

the cli crash when the project is not initialized

alfred --help will raise an exception TypeError: expected str, bytes or os.PathLike object, not NoneType on a project that haven't been initialized.

Expected behavior

A message show an error message and recommand an action to the user to initialize its project.

not an alfred project (or any of the parent directories), you should run alfred init command

alfred.flag decorator declares a flag on a command

the alfred.flag decorator declares a flag on an alfred command. This decorator allows you to specify all-or-nothing options.

Usage

A flag allows a developer to play a subpart of a command as with alfred ci --frontend. This flag allows running continuous integration only on the code of the frontend part of a project.

Implementation

@alfred.command("ci")
@alfred.flag("--frontend")
def continuous_integration(frontend: bool):
  pass

Short form of a flag

@alfred.command("ci")
@alfred.flag("--frontend", "-f")
def continuous_integration(frontend: bool):
  pass

Flag in negative logic

@alfred.command("ci")
@alfred.flag("--ignore-windows-tasks", default=True)
def continuous_integration(ignore_windows_tasks: bool):
  pass

Discarded alternative

a flag as a specific optional argument : click considers a flag to be a particular optional argument. this model makes full sense for click which is a toolkit for developing a command line. The counterpart is that this approach requires richer documentation. I regularly search the documentation for how to write a flag. 80% of the options argument I use with alfred are simple flags. I prefer to think of a flag as a dedicated instruction.

alfred --new assists the developer to create a new command

alfred --new trigger a wizard to create a new command. The wizard guides the developper in the process.

alfred --new
What command do you want to create ? 
$ ci

What does the `ci` command do [optional] ?
$

In which module do you want to create the `ci` command?
$ ci

Do you want to create the following `ci` command in `alfred/ci.py` [y, n]?

>>> @alfred.command('ci', help="")
>>> def ci():
>>> 	alfred.run("echo 'ci command not implemented yet'")

run virtual environment in pseudo terminal instead of subprocess on command invocation

A developer can launch a command which depends on a virtual environment other than the one he has activated in his terminal. In this case, alfred switch the virtual environment by itself but lost interactivity in the operation. Color and rich prompt are note supported anymore.

During this call, there is no need to capture standard output and error output. By ignoring the standard and error outputs, we can recover interactivity.

alfred tests

For commands invoked in a command, we keep the current mode to make the standard output and error output accessible at the end of the command.

@alfred.command('ci', help="execute continuous integration process of alfred")
@alfred.option('-v', '--verbose', is_flag=True)
def ci(verbose: bool):
    alfred.invoke_command('alfred_check')
    if alfred.is_posix():
        alfred.invoke_command('lint', verbose=verbose)
    else:
        print("linter is not supported on non posix platform as windows")

    alfred.invoke_command('tests', verbose=verbose)

alfred --check flag verifies that all commands are loaded

the alfred --check command checked that all commands are loaded. When calling, it loads all command modules. Those with errors are displayed.

command module "/home/{...}/alfred/ci.py" is not valid.
 SyntaxError : expected ':' (ci.py, line 6)

If at least one of the command modules is in error, the command exits with code 1 to trigger an error in the continuous integration.

Use alfred --check in alfred command

The alfred --check command has its place in a continuous integration workflow. It checks that the commands implemented in alfred are all usable. An user can use this check as a subcommand with the alfred.invoke_itself(check=True) function

@alfred.command('ci', help="execute continuous integration process of alfred")
@alfred.option('-v', '--verbose', is_flag=True)
def ci(verbose: bool):
    alfred.invoke_itself(check=True)
    alfred.invoke_command('lint', verbose=verbose)
    alfred.invoke_command('tests', verbose=verbose)

Expected documentation

show instructions to configure alfred completion in your profile with alfred --completion

The alfred --autocomplete command shows the user the shell statement to execute to enable autocomplete.

The shell supported are the one from click documentation :

  • bash
  • zsh
  • fish

The command displays after instructions to permanently configure autocompletion.

execute the following statement to activate autocompletion for alfred on bash (shell we have detected)

>>> eval "$(_ALFRED_COMPLETE=bash_source alfred)"

Adding this statement to your shell profile installs alfred's autocompletion permanently. 
You will avoid running this command every time you open a terminal window.

The profile file for bash is ~/.bashrc

run multiple commands in parallel - fastapi and npm for example

The contextmanager alfred.parallel allow a developper to run multiple commands simulatneously and in a single terminal. It's useful to run fullstack apps, for example to run npm, fastapi and a database in same time.

When the user interrupts a command with CTRL+C, all commands that are currently executing are interrupted. If it is another signal as sigquit, it is forwarded to the commands being executed by alfred.

Log messages are prefixed with the command name in square brackets like in docker-compose to be able to track what is happening.

[front] 2022-12-01 12:56:54 hello world

usage

The developer declare the commands to be executed simultaneously in an alfred.parallel context block. Each of the commands will run independently of each other.

with alfred.parallel() as p:
  p.run(npm, ["run"], name="front")
  p.run(python, ["run"], name="back")

The commands are waited for indefinitely before exiting the alfred.parallel block. It is possible to disable this behavior and explicitly check the end of a command by setting the attribute wait to False. The instruction wait allow to wait the end of a command explicitely.

with alfred.parallel(wait=False) as p:
  p.run(npm, ["run"], name="front")
  p.run(python, ["run"], name="back")
  p.wait(commands=['front', 'back'])

alfred commands can be invoked in an alfred.parallel block.

with alfred.parallel() as p:
  p.invoke_command("frontend:run", name="front")
  p.invoke_command("backend:run", args={'verbose': verbose}, "back")

primitives to verify the operating system

In the case of a cross-platform build, it is necessary to execute a different workflow between linux, windows and macos. For example, github action is limited on macOS and windows in using docker container. Excluding tests that use docker on both platform is a valid strategy if you can't host your own CI runner. That's the cas in FabienArcellier/fixtup.

the alfred package provides out-of-the-box functions to verify the os

alfred.is_posix()
alfred.is_linux()
alfred.is_macos()
alfred.is_windows()

stream command on windows

A subcommand executed in Windows is not streamed. The standard output is displayed intermittently at the end of each invocation.

fix in #56

show a prompt on reentrant interpreter with alfred.prompt

A confirmation requested with click.prompt is visited in a command which is executed by an interpreter launched by alfred.

@alfred.command("publish")
def publish():
  value = click.prompt("Confirm", type=Choice(['y', 'n']), show_choices=True, default='n')
  print(value)

alfred.sh is able to load shell program from project virtual environment as mypy

programs like mypy and pytest, installed in the virtual environment of the project, are not resolved by the functionalfred.sh.

mypy = alfred.sh("mypy", "mypy should be present")

Design

When alfred identifies that he must play a command in a virtual environment different from the one used, the path to the binaries of this virtual environment is added to the PATH

Discarded alternative

  • enable virtual environment at OS level : this method is complex to implement because it uses the shell. I have seen strange behaviors when a virtual environment is activated in another virtual environment.

command content is not buffered by python

The content of a command like streamsync is buffered. The end of the command is missing. The server address and port remains invisible.

export PYTHONUNBUFFERED=1 make them visible.

detect a virtual environment managed with poetry

  • ignore this workflow when venv_poetry_ignore is set
  • detect the presence of poetry inside the pyproject.toml
  • detect the presence of poetry executable
  • display warning if poetry is in pyproject.toml and poetry executable is missing
Poetry manages this project. Poetry is missing from your system.
https://python-poetry.org/docs/#installation
  • detect the venv using poetry

detect poetry environment with pyenv

When pyenv is used, poetry cannot identify the virtual environment attached to the project with the poetry env info command. It manages to list virtual environments and activate its shell.

Alfred cannot change environments because he does not have a reliable way to identify the currently installed virtual environment.

2024-02-13 20:04:18,652 WARNING - Poetry virtual environment is missing. You should run poetry install. [poetry.py:39]

running any alfred command on directory that are not initialized should show simple error message

Running any command on an uninitialized project should show an error message, but not an exception :

not an alfred project (or any of the parent directories), you should run alfred init

Currently alfred --new echo test displays a complete exception:

Traceback (most recent call last):
  File "/home/far/.local/bin/alfred", line 8, in <module>
    sys.exit(cli())
  File "/home/far/.local/lib/python3.10/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/home/far/.local/lib/python3.10/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/home/far/documents/projects/20210821_1530__alfred-cli/src/alfred/cli.py", line 118, in invoke
    self_command.new(fullarg)
  File "/home/far/documents/projects/20210821_1530__alfred-cli/src/alfred/self_command.py", line 31, in new
    project_dir = manifest.lookup_project_dir()
  File "/home/far/documents/projects/20210821_1530__alfred-cli/src/alfred/manifest.py", line 67, in lookup_project_dir
    raise NotInitialized("not an alfred project (or any of the parent directories), you should run alfred init")
alfred.exceptions.NotInitialized: not an alfred project (or any of the parent directories), you should run alfred init

add path in alfred manifest to manage fullstack app that contains utility install with nodejs

Nodejs installs utilities like jest in the node_modules folder. These utilities are in the node_modules/.bin folder. To use them directly, a developer must insert them into their PATH.

[alfred]

[alfred.project]
path = [] # optional

Adding elements to the PATH from an Alfred manifest allows you to use the commands directly.

import alfred

@alfred.command('frontend:test', help="tests the frontend")
def tests():
    jest = alfred.sh('jest', "jest is not installed in your system")
    alfred.run(jest)

definition of done

  • implement path support in _context_middleware
  • document path on section Project

cannot import name 'text_encoding' from 'io'

On certain environments, alfred triggers this error. I removed the dependency which was not essential.

Fatal Python error: init_sys_streams: can't initialize sys standard streams
Python runtime state: core initialized
Traceback (most recent call last):
  File "/home/debian/.pyenv/versions/3.10.4/lib/python3.10/io.py", line 54, in <module>
ImportError: cannot import name 'text_encoding' from 'io' (unknown location)

ae478d2

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.