Skip to content

Commit

Permalink
feat: Provide callables for popular tools
Browse files Browse the repository at this point in the history
Issue #7: #7
  • Loading branch information
pawamoy committed Feb 18, 2023
1 parent 629b988 commit 0e065e2
Show file tree
Hide file tree
Showing 15 changed files with 3,601 additions and 3 deletions.
20 changes: 18 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ It is automatically created and passed to your function.
It has only one purpose: running command with its `run` method.
The `run` method accepts strings, list of strings, or even Python callables.

The above duty can be rewritten as:
The above duty runs the command in a shell process.
To avoid using a shell, pass a list of strings instead:

```python
from duty import duty
Expand All @@ -38,7 +39,7 @@ def docs(ctx):
# avoid the overhead of an extra shell process
```

Or:
And to avoid using a subprocess completely, pass a Python callable:

```python
from duty import duty
Expand All @@ -50,6 +51,21 @@ def docs(ctx):
# avoid the overhead of an extra Python process
```

For convenience, `duty` provides callables for many popular Python tools,
so that you don't have to read their source and learn how to call them.
For example, the `mkdocs build` command can be called like this:

```python
from duty import duty
from duty.callables import mkdocs

@duty
def docs(ctx):
ctx.run(mkdocs.build, kwargs={"strict": True}, title="Building documentation")
```

**[See all our callables in the Code reference][duty.callables].**

### `ctx.run()` options

The `run` methods accepts various options,
Expand Down
2 changes: 1 addition & 1 deletion scripts/gen_ref_nav.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
parts = parts[:-1]
doc_path = doc_path.with_name("index.md")
full_doc_path = full_doc_path.with_name("index.md")
elif parts[-1] == "__main__":
elif parts[-1].startswith("_"):
continue

nav[parts] = doc_path.as_posix()
Expand Down
42 changes: 42 additions & 0 deletions src/duty/callables/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Module containing callables for many tools.
Tip: Call to developers!
If you are the author or maintainer of one of the tools we support
(or more generally if you are the author/maintainer of a Python CLI/library),
we kindly request that you add such a callable to your code base. Why?
- Most of the time, all `duty` can do is hook into the CLI entrypoint
for the lack of a better alternative. This is not ideal because
we have to translate function arguments to CLI arguments,
that are then parsed again and translated back to Python objects
by the tool itself. This is not efficient.
- It is not feasible for `duty` to maintain callables for different versions
of these tools. Having the callables maintained in the tools
themselves would make this support transparent.
- We believe it simply provides a better user- *and* developer-experience.
Clear separation of concerns: don't intertwine logic into the CLI parser.
Easy to maintain, easy to test. The CLI parser just has to translate CLI args
to their equivalent Python arguments.
Tips for writing such a library entry point:
- Make it equivalent to the CLI entry point: every flag and option must have an equivalent parameter.
Slight customizations can be made to support `--flag` / `--no-flag` with single parameters.
- Use only built-in types: don't make users import and use objects from your API.
For example, accept a list of strings, not a list of `MyCustomClass` instances.
"""

from functools import wraps
from typing import Callable


def _named(name: str) -> Callable:
def decorator(func: Callable) -> Callable:
@wraps(func)
def inner(*args, **kwargs): # noqa: ANN002,ANN003,ANN202
return func(*args, **kwargs)

inner.__name__ = name
return inner

return decorator
18 changes: 18 additions & 0 deletions src/duty/callables/_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import sys
from io import StringIO


class _LazyStdout(StringIO):
def __repr__(self) -> str:
return "stdout"

def write(self, value: str) -> int:
return sys.stdout.write(value)


class _LazyStderr(StringIO):
def __repr__(self) -> str:
return "stderr"

def write(self, value: str) -> int:
return sys.stderr.write(value)
126 changes: 126 additions & 0 deletions src/duty/callables/autoflake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""Callable for [autoflake](https://github.com/PyCQA/autoflake)."""

from __future__ import annotations

from autoflake import _main as autoflake

from duty.callables import _io, _named


@_named("autoflake")
def run(
*files: str,
config: str | None = None,
check: bool | None = None,
check_diff: bool | None = None,
imports: list[str] | None = None,
remove_all_unused_imports: bool | None = None,
recursive: bool | None = None,
jobs: int | None = None,
exclude: list[str] | None = None,
expand_star_imports: bool | None = None,
ignore_init_module_imports: bool | None = None,
remove_duplicate_keys: bool | None = None,
remove_unused_variables: bool | None = None,
remove_rhs_for_unused_variables: bool | None = None,
ignore_pass_statements: bool | None = None,
ignore_pass_after_docstring: bool | None = None,
quiet: bool | None = None,
verbose: bool | None = None,
stdin_display_name: str | None = None,
in_place: bool | None = None,
stdout: bool | None = None,
) -> int:
r"""Run `autoflake`.
Parameters:
*files: Files to format.
config: Explicitly set the config file instead of auto determining based on file location.
check: Return error code if changes are needed.
check_diff: Return error code if changes are needed, also display file diffs.
imports: By default, only unused standard library imports are removed; specify a comma-separated list of additional modules/packages.
remove_all_unused_imports: Remove all unused imports (not just those from the standard library).
recursive: Drill down directories recursively.
jobs: Number of parallel jobs; match CPU count if value is 0 (default: 0).
exclude: Exclude file/directory names that match these comma-separated globs.
expand_star_imports: Expand wildcard star imports with undefined names; this only triggers if there is only one star import in the file; this is skipped if there are any uses of `__all__` or `del` in the file.
ignore_init_module_imports: Exclude `__init__.py` when removing unused imports.
remove_duplicate_keys: Remove all duplicate keys in objects.
remove_unused_variables: Remove unused variables.
remove_rhs_for_unused_variables: Remove RHS of statements when removing unused variables (unsafe).
ignore_pass_statements: Ignore all pass statements.
ignore_pass_after_docstring: Ignore pass statements after a newline ending on `\"\"\"`.
quiet: Suppress output if there are no issues.
verbose: Print more verbose logs (you can repeat `-v` to make it more verbose).
stdin_display_name: The name used when processing input from stdin.
in_place: Make changes to files instead of printing diffs.
stdout: Print changed text to stdout. defaults to true when formatting stdin, or to false otherwise.
"""
cli_args = list(files)

if check:
cli_args.append("--check")

if check_diff:
cli_args.append("--check-diff")

if imports:
cli_args.append("--imports")
cli_args.append(",".join(imports))

if remove_all_unused_imports:
cli_args.append("--remove-all-unused-imports")

if recursive:
cli_args.append("--recursive")

if jobs:
cli_args.append("--jobs")
cli_args.append(str(jobs))

if exclude:
cli_args.append("--exclude")
cli_args.append(",".join(exclude))

if expand_star_imports:
cli_args.append("--expand-star-imports")

if ignore_init_module_imports:
cli_args.append("--ignore-init-module-imports")

if remove_duplicate_keys:
cli_args.append("--remove-duplicate-keys")

if remove_unused_variables:
cli_args.append("--remove-unused-variables")

if remove_rhs_for_unused_variables:
cli_args.append("remove-rhs-for-unused-variables")

if ignore_pass_statements:
cli_args.append("--ignore-pass-statements")

if ignore_pass_after_docstring:
cli_args.append("--ignore-pass-after-docstring")

if quiet:
cli_args.append("--quiet")

if verbose:
cli_args.append("--verbose")

if stdin_display_name:
cli_args.append("--stdin-display-name")
cli_args.append(stdin_display_name)

if config:
cli_args.append("--config")
cli_args.append(config)

if in_place:
cli_args.append("--in-place")

if stdout:
cli_args.append("--stdout")

return autoflake(cli_args, standard_out=_io._LazyStdout(), standard_error=_io._LazyStderr()) # noqa: SLF001
Loading

0 comments on commit 0e065e2

Please sign in to comment.