diff --git a/docs/usage.md b/docs/usage.md index 7c12239..3dd4b20 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -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 @@ -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 @@ -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, diff --git a/scripts/gen_ref_nav.py b/scripts/gen_ref_nav.py old mode 100755 new mode 100644 index 2d8a4c3..358ee3e --- a/scripts/gen_ref_nav.py +++ b/scripts/gen_ref_nav.py @@ -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() diff --git a/src/duty/callables/__init__.py b/src/duty/callables/__init__.py new file mode 100644 index 0000000..f972ea7 --- /dev/null +++ b/src/duty/callables/__init__.py @@ -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 diff --git a/src/duty/callables/_io.py b/src/duty/callables/_io.py new file mode 100644 index 0000000..7266ab4 --- /dev/null +++ b/src/duty/callables/_io.py @@ -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) diff --git a/src/duty/callables/autoflake.py b/src/duty/callables/autoflake.py new file mode 100644 index 0000000..b787696 --- /dev/null +++ b/src/duty/callables/autoflake.py @@ -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 diff --git a/src/duty/callables/black.py b/src/duty/callables/black.py new file mode 100644 index 0000000..354c741 --- /dev/null +++ b/src/duty/callables/black.py @@ -0,0 +1,176 @@ +"""Callable for [Black](https://github.com/psf/black).""" + +from __future__ import annotations + +from black import main as black + +from duty.callables import _named + + +@_named("black") +def run( + *src: str, + config: str | None = None, + code: str | None = None, + line_length: int | None = None, + target_version: str | None = None, + check: bool | None = None, + diff: bool | None = None, + color: bool | None = None, + fast: bool | None = None, + pyi: bool | None = None, + ipynb: bool | None = None, + python_cell_magics: str | None = None, + skip_source_first_line: bool | None = None, + skip_string_normalization: bool | None = None, + skip_magic_trailing_comma: bool | None = None, + experimental_string_processing: bool | None = None, + preview: bool | None = None, + quiet: bool | None = None, + verbose: bool | None = None, + required_version: str | None = None, + include: str | None = None, + exclude: str | None = None, + extend_exclude: str | None = None, + force_exclude: str | None = None, + stdin_filename: str | None = None, + workers: int | None = None, +) -> None: + r"""Run `black`. + + Parameters: + src: Format the directories and file paths. + config: Read configuration from this file path. + code: Format the code passed in as a string. + line_length: How many characters per line to allow [default: 120]. + target_version: Python versions that should be supported by Black's output. + By default, Black will try to infer this from the project metadata in pyproject.toml. + If this does not yield conclusive results, Black will use per-file auto-detection. + check: Don't write the files back, just return the status. Return code 0 means nothing would change. + Return code 1 means some files would be reformatted. Return code 123 means there was an internal error. + diff: Don't write the files back, just output a diff for each file on stdout. + color: Show colored diff. Only applies when `--diff` is given. + fast: If --fast given, skip temporary sanity checks. [default: --safe] + pyi: Format all input files like typing stubs regardless of file extension + (useful when piping source on standard input). + ipynb: Format all input files like Jupyter Notebooks regardless of file extension + (useful when piping source on standard input). + python_cell_magics: When processing Jupyter Notebooks, add the given magic to the list of known python-magics + (capture, prun, pypy, python, python3, time, timeit). Useful for formatting cells with custom python magics. + skip_source_first_line: Skip the first line of the source code. + skip_string_normalization: Don't normalize string quotes or prefixes. + skip_magic_trailing_comma: Don't use trailing commas as a reason to split lines. + preview: Enable potentially disruptive style changes that may be added + to Black's main functionality in the next major release. + quiet: Don't emit non-error messages to stderr. Errors are still emitted; silence those with 2>/dev/null. + verbose: Also emit messages to stderr about files that were not changed or were ignored due to exclusion patterns. + required_version: Require a specific version of Black to be running (useful for unifying results + across many environments e.g. with a pyproject.toml file). + It can be either a major version number or an exact version. + include: A regular expression that matches files and directories that should be included on recursive searches. + An empty value means all files are included regardless of the name. Use forward slashes for directories + on all platforms (Windows, too). Exclusions are calculated first, inclusions later [default: (\.pyi?|\.ipynb)$]. + exclude: A regular expression that matches files and directories that should be excluded on recursive searches. + An empty value means no paths are excluded. Use forward slashes for directories on all platforms (Windows, too). + Exclusions are calculated first, inclusions later [default: /(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox| + \.tox|\.venv|venv|\.svn|\.ipynb_checkpoints|_build|buck-out|build|dist|__pypackages__)/]. + extend_exclude: Like --exclude, but adds additional files and directories on top of the excluded ones + (useful if you simply want to add to the default). + force_exclude: Like --exclude, but files and directories matching this regex will be excluded + even when they are passed explicitly as arguments. + stdin_filename: The name of the file when passing it through stdin. Useful to make sure Black will respect + --force-exclude option on some editors that rely on using stdin. + workers: Number of parallel workers [default: number CPUs in the system]. + """ + cli_args = list(src) + + if config: + cli_args.append("--config") + cli_args.append(config) + + if code: + cli_args.append("--code") + cli_args.append(code) + + if line_length: + cli_args.append("--line-length") + cli_args.append(str(line_length)) + + if target_version: + cli_args.append("--target-version") + cli_args.append(target_version) + + if check: + cli_args.append("--check") + + if diff: + cli_args.append("--diff") + + if color is True: + cli_args.append("--color") + elif color is False: + cli_args.append("--no-color") + + if fast: + cli_args.append("--fast") + + if pyi: + cli_args.append("--pyi") + + if ipynb: + cli_args.append("--ipynb") + + if python_cell_magics: + cli_args.append("--python-cell-magics") + cli_args.append(python_cell_magics) + + if skip_source_first_line: + cli_args.append("--skip_source_first_line") + + if skip_string_normalization: + cli_args.append("--skip_string_normalization") + + if skip_magic_trailing_comma: + cli_args.append("--skip_magic_trailing_comma") + + if experimental_string_processing: + cli_args.append("--experimental_string_processing") + + if preview: + cli_args.append("--preview") + + if quiet: + cli_args.append("--quiet") + + if verbose: + cli_args.append("--verbose") + + if required_version: + cli_args.append("--required-version") + cli_args.append(required_version) + + if include: + cli_args.append("--include") + cli_args.append(include) + + if exclude: + cli_args.append("--exclude") + cli_args.append(exclude) + + if extend_exclude: + cli_args.append("--extend-exclude") + cli_args.append(extend_exclude) + + if force_exclude: + cli_args.append("--force-exclude") + cli_args.append(force_exclude) + + if stdin_filename: + cli_args.append("--stdin-filename") + cli_args.append(stdin_filename) + + if workers: + cli_args.append("--workers") + cli_args.append(str(workers)) + + return black(cli_args, prog_name="black") diff --git a/src/duty/callables/blacken_docs.py b/src/duty/callables/blacken_docs.py new file mode 100644 index 0000000..5d41dfd --- /dev/null +++ b/src/duty/callables/blacken_docs.py @@ -0,0 +1,85 @@ +"""Callable for [blacken-docs](https://github.com/adamchainz/blacken-docs).""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING, Pattern, Sequence + +import black +from blacken_docs import format_file + +from duty.callables import _named + +if TYPE_CHECKING: + from pathlib import Path + + +@_named("blacken_docs") +def run( + *paths: Path, + exts: Sequence[str] | None = None, + exclude: Sequence[str | Pattern] | None = None, + skip_errors: bool = False, + rst_literal_blocks: bool = False, + line_length: int = black.DEFAULT_LINE_LENGTH, + string_normalization: bool = True, + is_pyi: bool = False, + is_ipynb: bool = False, + skip_source_first_line: bool = False, + magic_trailing_comma: bool = True, + python_cell_magics: set[str] | None = None, + preview: bool = False, +) -> int: + """Run `blacken-docs`. + + Parameters: + *paths: Directories and files to format. + exts: List of extensions to select files with. + exclude: List of regular expressions to exclude files. + skip_errors: Don't exit non-zero for errors from Black (normally syntax errors). + rst_literal_blocks: Also format literal blocks in reStructuredText files (more below). + line_length: How many characters per line to allow. + string_normalization: Normalize string quotes or prefixes. + is_pyi: Format all input files like typing stubs regardless of file extension. + is_ipynb: Format all input files like Jupyter Notebooks regardless of file extension. + skip_source_first_line: Skip the first line of the source code. + magic_trailing_comma: Use trailing commas as a reason to split lines. + python_cell_magics: When processing Jupyter Notebooks, add the given magic to the list + of known python-magics (capture, prun, pypy, python, python3, time, timeit). + Useful for formatting cells with custom python magics. + preview: Enable potentially disruptive style changes that may be added + to Black's main functionality in the next major release. + + Returns: + Success/failure. + """ + exts = ("md", "py") if exts is None else tuple(ext.lstrip(".") for ext in exts) + if exclude: + exclude = tuple(re.compile(regex, re.I) if isinstance(regex, str) else regex for regex in exclude) + filepaths = set() + for path in paths: + if path.is_file(): + filepaths.add(path.as_posix()) + else: + for ext in exts: + filepaths |= {filepath.as_posix() for filepath in path.rglob(f"*.{ext}")} + + black_mode = black.Mode( + line_length=line_length, + string_normalization=string_normalization, + is_pyi=is_pyi, + is_ipynb=is_ipynb, + skip_source_first_line=skip_source_first_line, + magic_trailing_comma=magic_trailing_comma, + python_cell_magics=python_cell_magics or set(), + preview=preview, + ) + retv = 0 + for filepath in sorted(filepaths): + retv |= format_file( + filepath, + black_mode, + skip_errors=skip_errors, + rst_literal_blocks=rst_literal_blocks, + ) + return retv diff --git a/src/duty/callables/coverage.py b/src/duty/callables/coverage.py new file mode 100644 index 0000000..dd531ff --- /dev/null +++ b/src/duty/callables/coverage.py @@ -0,0 +1,718 @@ +"""Callable for [Coverage.py](https://github.com/nedbat/coveragepy).""" + +from __future__ import annotations + +import sys + +from coverage.cmdline import main as coverage + +from duty.callables import _named + +# TODO: remove once support for Python 3.7 is dropped +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal + + +@_named("coverage.annotate") +def annotate( + *, + rcfile: str | None = None, + directory: str | None = None, + data_file: str | None = None, + ignore_errors: bool | None = None, + include: list[str] | None = None, + omit: list[str] | None = None, + debug_opts: list[str] | None = None, +) -> None: + """Annotate source files with execution information. + + Make annotated copies of the given files, marking statements that are executed + with `>` and statements that are missed with `!`. + + Parameters: + rcfile: Specify configuration file. By default `.coveragerc`, `setup.cfg`, `tox.ini`, + and `pyproject.toml` are tried [env: `COVERAGE_RCFILE`]. + directory: Write the output files to this directory. + data_file: Read coverage data for report generation from this file. + Defaults to `.coverage` [env: `COVERAGE_FILE`]. + ignore_errors: Ignore errors while reading source files. + include: Include only files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + omit: Omit files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + debug_opts: Debug options, separated by commas [env: `COVERAGE_DEBUG`]. + """ + cli_args = ["annotate"] + + if directory: + cli_args.append("--directory") + cli_args.append(directory) + + if data_file: + cli_args.append("--data-file") + cli_args.append(data_file) + + if ignore_errors: + cli_args.append("--ignore-errors") + + if include: + cli_args.append("--include") + cli_args.append(",".join(include)) + + if omit: + cli_args.append("--omit") + cli_args.append(",".join(omit)) + + if debug_opts: + cli_args.append("--debug") + cli_args.append(",".join(debug_opts)) + + if rcfile: + cli_args.append("--rcfile") + cli_args.append(rcfile) + + coverage(cli_args) + + +@_named("coverage.combine") +def combine( + *paths: str, + rcfile: str | None = None, + append: bool | None = None, + data_file: str | None = None, + keep: bool | None = None, + quiet: bool | None = None, + debug_opts: list[str] | None = None, +) -> None: + """Combine a number of data files. + + Combine data from multiple coverage files. The combined results are written to + a single file representing the union of the data. The positional arguments are + data files or directories containing data files. If no paths are provided, + data files in the default data file's directory are combined. + + Parameters: + paths: Paths to combine. + rcfile: Specify configuration file. By default `.coveragerc`, `setup.cfg`, `tox.ini`, + and `pyproject.toml` are tried [env: `COVERAGE_RCFILE`]. + append: Append coverage data to .coverage, otherwise it starts clean each time. + data_file: Read coverage data for report generation from this file. + Defaults to `.coverage` [env: `COVERAGE_FILE`]. + keep: Keep original coverage files, otherwise they are deleted. + quiet: Don't print messages about what is happening. + debug_opts: Debug options, separated by commas [env: `COVERAGE_DEBUG`]. + """ + cli_args = ["combine", *paths] + + if append: + cli_args.append("--append") + + if data_file: + cli_args.append("--data-file") + cli_args.append(data_file) + + if keep: + cli_args.append("--keep") + + if quiet: + cli_args.append("--quiet") + + if debug_opts: + cli_args.append("--debug") + cli_args.append(",".join(debug_opts)) + + if rcfile: + cli_args.append("--rcfile") + cli_args.append(rcfile) + + coverage(cli_args) + + +@_named("coverage.debug") +def debug( + topic: Literal["data", "sys", "config", "premain", "pybehave"], + *, + rcfile: str | None = None, + debug_opts: list[str] | None = None, +) -> None: + """Display information about the internals of coverage.py. + + Display information about the internals of coverage.py, for diagnosing + problems. Topics are: `data` to show a summary of the collected data; `sys` to + show installation information; `config` to show the configuration; `premain` + to show what is calling coverage; `pybehave` to show internal flags describing + Python behavior. + + Parameters: + topic: Topic to display. + rcfile: Specify configuration file. By default `.coveragerc`, `setup.cfg`, `tox.ini`, + and `pyproject.toml` are tried [env: `COVERAGE_RCFILE`]. + debug_opts: Debug options, separated by commas [env: `COVERAGE_DEBUG`]. + """ + cli_args: list[str] = ["debug", topic] + + if debug_opts: + cli_args.append("--debug") + cli_args.append(",".join(debug_opts)) + + if rcfile: + cli_args.append("--rcfile") + cli_args.append(rcfile) + + coverage(cli_args) + + +@_named("coverage.erase") +def erase( + *, + rcfile: str | None = None, + data_file: str | None = None, + debug_opts: list[str] | None = None, +) -> None: + """Erase previously collected coverage data. + + Parameters: + rcfile: Specify configuration file. By default `.coveragerc`, `setup.cfg`, `tox.ini`, + and `pyproject.toml` are tried [env: `COVERAGE_RCFILE`]. + data_file: Read coverage data for report generation from this file. + Defaults to `.coverage` [env: `COVERAGE_FILE`]. + debug_opts: Debug options, separated by commas [env: `COVERAGE_DEBUG`]. + """ + cli_args = ["erase"] + + if data_file: + cli_args.append("--data-file") + cli_args.append(data_file) + + if debug_opts: + cli_args.append("--debug") + cli_args.append(",".join(debug_opts)) + + if rcfile: + cli_args.append("--rcfile") + cli_args.append(rcfile) + + coverage(cli_args) + + +@_named("coverage.html") +def html( + *, + rcfile: str | None = None, + contexts: list[str] | None = None, + directory: str | None = None, + data_file: str | None = None, + fail_under: int | None = None, + ignore_errors: bool | None = None, + include: list[str] | None = None, + omit: list[str] | None = None, + precision: int | None = None, + quiet: bool | None = None, + show_contexts: bool | None = None, + skip_covered: bool | None = None, + skip_empty: bool | None = None, + title: str | None = None, + debug_opts: list[str] | None = None, +) -> None: + """Create an HTML report. + + Create an HTML report of the coverage of the files. Each file gets its own + page, with the source decorated to show executed, excluded, and missed lines. + + Parameters: + rcfile: Specify configuration file. By default `.coveragerc`, `setup.cfg`, `tox.ini`, + and `pyproject.toml` are tried [env: `COVERAGE_RCFILE`]. + contexts: Only display data from lines covered in the given contexts. + Accepts Python regexes, which must be quoted. + directory: Write the output files to this directory. + data_file: Read coverage data for report generation from this file. + Defaults to `.coverage` [env: `COVERAGE_FILE`]. + fail_under: Exit with a status of 2 if the total coverage is less than the given number. + ignore_errors: Ignore errors while reading source files. + include: Include only files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + omit: Omit files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + precision: Number of digits after the decimal point to display for reported coverage percentages. + quiet: Don't print messages about what is happening. + show_contexts: Show contexts for covered lines. + skip_covered: Skip files with 100% coverage. + skip_empty: Skip files with no code. + title: A text string to use as the title on the HTML. + debug_opts: Debug options, separated by commas [env: `COVERAGE_DEBUG`]. + """ + cli_args = ["html"] + + if contexts: + cli_args.append("--contexts") + cli_args.append(",".join(contexts)) + + if directory: + cli_args.append("--directory") + cli_args.append(directory) + + if data_file: + cli_args.append("--data-file") + cli_args.append(data_file) + + if fail_under is not None: + cli_args.append("--fail-under") + cli_args.append(str(fail_under)) + + if ignore_errors: + cli_args.append("--ignore-errors") + + if include: + cli_args.append("--include") + cli_args.append(",".join(include)) + + if omit: + cli_args.append("--omit") + cli_args.append(",".join(omit)) + + if precision is not None: + cli_args.append("--precision") + cli_args.append(str(precision)) + + if quiet: + cli_args.append("--quiet") + + if show_contexts: + cli_args.append("--show-contexts") + + if skip_covered is True: + cli_args.append("--skip-covered") + elif skip_covered is False: + cli_args.append("--no-skip-covered") + + if skip_empty: + cli_args.append("--skip-empty") + + if title: + cli_args.append("--title") + cli_args.append(title) + + if debug_opts: + cli_args.append("--debug") + cli_args.append(",".join(debug_opts)) + + if rcfile: + cli_args.append("--rcfile") + cli_args.append(rcfile) + + coverage(cli_args) + + +@_named("coverage.json") +def json( + *, + rcfile: str | None = None, + contexts: list[str] | None = None, + data_file: str | None = None, + fail_under: int | None = None, + ignore_errors: bool | None = None, + include: list[str] | None = None, + omit: list[str] | None = None, + output: str | None = None, + pretty_print: bool | None = None, + quiet: bool | None = None, + show_contexts: bool | None = None, + debug_opts: list[str] | None = None, +) -> None: + """Create a JSON report of coverage results. + + Parameters: + rcfile: Specify configuration file. By default `.coveragerc`, `setup.cfg`, `tox.ini`, + and `pyproject.toml` are tried [env: `COVERAGE_RCFILE`]. + contexts: Only display data from lines covered in the given contexts. + Accepts Python regexes, which must be quoted. + data_file: Read coverage data for report generation from this file. + Defaults to `.coverage` [env: `COVERAGE_FILE`]. + fail_under: Exit with a status of 2 if the total coverage is less than the given number. + ignore_errors: Ignore errors while reading source files. + include: Include only files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + omit: Omit files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + output: Write the JSON report to this file. Defaults to `coverage.json`. + pretty_print: Format the JSON for human readers. + quiet: Don't print messages about what is happening. + show_contexts: Show contexts for covered lines. + debug_opts: Debug options, separated by commas [env: `COVERAGE_DEBUG`]. + """ + cli_args = ["json"] + + if contexts: + cli_args.append("--contexts") + cli_args.append(",".join(contexts)) + + if data_file: + cli_args.append("--data-file") + cli_args.append(data_file) + + if fail_under is not None: + cli_args.append("--fail-under") + cli_args.append(str(fail_under)) + + if ignore_errors: + cli_args.append("--ignore-errors") + + if include: + cli_args.append("--include") + cli_args.append(",".join(include)) + + if omit: + cli_args.append("--omit") + cli_args.append(",".join(omit)) + + if output: + cli_args.append("-o") + cli_args.append(output) + + if pretty_print: + cli_args.append("--pretty-print") + + if quiet: + cli_args.append("--quiet") + + if show_contexts: + cli_args.append("--show-contexts") + + if debug_opts: + cli_args.append("--debug") + cli_args.append(",".join(debug_opts)) + + if rcfile: + cli_args.append("--rcfile") + cli_args.append(rcfile) + + coverage(cli_args) + + +@_named("coverage.lcov") +def lcov( + *, + rcfile: str | None = None, + data_file: str | None = None, + fail_under: int | None = None, + ignore_errors: bool | None = None, + include: list[str] | None = None, + omit: list[str] | None = None, + output: str | None = None, + quiet: bool | None = None, + debug_opts: list[str] | None = None, +) -> None: + """Create an LCOV report of coverage results. + + Parameters: + rcfile: Specify configuration file. By default `.coveragerc`, `setup.cfg`, `tox.ini`, + and `pyproject.toml` are tried [env: `COVERAGE_RCFILE`]. + data_file: Read coverage data for report generation from this file. + Defaults to `.coverage` [env: `COVERAGE_FILE`]. + fail_under: Exit with a status of 2 if the total coverage is less than the given number. + ignore_errors: Ignore errors while reading source files. + include: Include only files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + omit: Omit files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + output: Write the JSON report to this file. Defaults to `coverage.json`. + quiet: Don't print messages about what is happening. + debug_opts: Debug options, separated by commas [env: `COVERAGE_DEBUG`]. + """ + cli_args = ["lcov"] + + if data_file: + cli_args.append("--data-file") + cli_args.append(data_file) + + if fail_under is not None: + cli_args.append("--fail-under") + cli_args.append(str(fail_under)) + + if ignore_errors: + cli_args.append("--ignore-errors") + + if include: + cli_args.append("--include") + cli_args.append(",".join(include)) + + if omit: + cli_args.append("--omit") + cli_args.append(",".join(omit)) + + if output: + cli_args.append("-o") + cli_args.append(output) + + if quiet: + cli_args.append("--quiet") + + if debug_opts: + cli_args.append("--debug") + cli_args.append(",".join(debug_opts)) + + if rcfile: + cli_args.append("--rcfile") + cli_args.append(rcfile) + + coverage(cli_args) + + +@_named("coverage.report") +def report( + *, + rcfile: str | None = None, + contexts: list[str] | None = None, + data_file: str | None = None, + fail_under: int | None = None, + output_format: Literal["text", "markdown", "total"] | None = None, + ignore_errors: bool | None = None, + include: list[str] | None = None, + omit: list[str] | None = None, + precision: int | None = None, + sort: Literal["name", "stmts", "miss", "branch", "brpart", "cover"] | None = None, + show_missing: bool | None = None, + skip_covered: bool | None = None, + skip_empty: bool | None = None, + debug_opts: list[str] | None = None, +) -> None: + """Report coverage statistics on modules. + + Parameters: + rcfile: Specify configuration file. By default `.coveragerc`, `setup.cfg`, `tox.ini`, + and `pyproject.toml` are tried [env: `COVERAGE_RCFILE`]. + contexts: Only display data from lines covered in the given contexts. + data_file: Read coverage data for report generation from this file. + Defaults to `.coverage` [env: `COVERAGE_FILE`]. + fail_under: Exit with a status of 2 if the total coverage is less than the given number. + output_format: Output format, either text (default), markdown, or total. + ignore_errors: Ignore errors while reading source files. + include: Include only files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + omit: Omit files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + precision: Number of digits after the decimal point to display for reported coverage percentages. + sort: Sort the report by the named column: name, stmts, miss, branch, brpart, or cover. Default is name. + show_missing: Show line numbers of statements in each module that weren't executed. + skip_covered: Skip files with 100% coverage. + skip_empty: Skip files with no code. + debug_opts: Debug options, separated by commas [env: `COVERAGE_DEBUG`]. + """ + cli_args = ["report"] + + if contexts: + cli_args.append("--contexts") + cli_args.append(",".join(contexts)) + + if data_file: + cli_args.append("--data-file") + cli_args.append(data_file) + + if fail_under is not None: + cli_args.append("--fail-under") + cli_args.append(str(fail_under)) + + if output_format: + cli_args.append("--format") + cli_args.append(output_format) + + if ignore_errors: + cli_args.append("--ignore-errors") + + if include: + cli_args.append("--include") + cli_args.append(",".join(include)) + + if omit: + cli_args.append("--omit") + cli_args.append(",".join(omit)) + + if precision is not None: + cli_args.append("--precision") + cli_args.append(str(precision)) + + if sort: + cli_args.append("--sort") + cli_args.append(sort) + + if show_missing: + cli_args.append("--show-missing") + + if skip_covered is True: + cli_args.append("--skip-covered") + elif skip_covered is False: + cli_args.append("--no-skip-covered") + + if skip_empty: + cli_args.append("--skip-empty") + + if debug_opts: + cli_args.append("--debug") + cli_args.append(",".join(debug_opts)) + + if rcfile: + cli_args.append("--rcfile") + cli_args.append(rcfile) + + coverage(cli_args) + + +@_named("coverage.run") +def run( + pyfile: str, + *, + rcfile: str | None = None, + append: bool | None = None, + branch: bool | None = None, + concurrency: list[str] | None = None, + context: str | None = None, + data_file: str | None = None, + include: list[str] | None = None, + omit: list[str] | None = None, + module: bool | None = None, + pylib: bool | None = None, + parallel_mode: bool | None = None, + source: list[str] | None = None, + timid: bool | None = None, + debug_opts: list[str] | None = None, +) -> None: + """Run a Python program and measure code execution. + + Parameters: + pyfile: Python script or module to run. + rcfile: Specify configuration file. By default `.coveragerc`, `setup.cfg`, `tox.ini`, + and `pyproject.toml` are tried [env: `COVERAGE_RCFILE`]. + append: Append coverage data to .coverage, otherwise it starts clean each time. + branch: Measure branch coverage in addition to statement coverage. + concurrency: Properly measure code using a concurrency library. Valid values are: + eventlet, gevent, greenlet, multiprocessing, thread, or a comma-list of them. + context: The context label to record for this coverage run. + data_file: Read coverage data for report generation from this file. + Defaults to `.coverage` [env: `COVERAGE_FILE`]. + include: Include only files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + omit: Omit files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + module: The given file is an importable Python module, not a script path, to be run as `python -m` would run it. + pylib: Measure coverage even inside the Python installed library, which isn't done by default. + parallel_mode: Append the machine name, process id and random number to the data file name + to simplify collecting data from many processes. + source: A list of directories or importable names of code to measure. + timid: Use a simpler but slower trace method. Try this if you get seemingly impossible results! + debug_opts: Debug options, separated by commas [env: `COVERAGE_DEBUG`]. + """ + cli_args = ["run", pyfile] + + if append: + cli_args.append("--append") + + if branch: + cli_args.append("--branch") + + if concurrency: + cli_args.append("--concurrency") + cli_args.append(",".join(concurrency)) + + if context: + cli_args.append("--context") + cli_args.append(context) + + if data_file: + cli_args.append("--data-file") + cli_args.append(data_file) + + if include: + cli_args.append("--include") + cli_args.append(",".join(include)) + + if omit: + cli_args.append("--omit") + cli_args.append(",".join(omit)) + + if module: + cli_args.append("--module") + + if pylib: + cli_args.append("--pylib") + + if parallel_mode: + cli_args.append("--parallel-mode") + + if source: + cli_args.append("--source") + cli_args.append(",".join(source)) + + if timid: + cli_args.append("--timid") + + if debug_opts: + cli_args.append("--debug") + cli_args.append(",".join(debug_opts)) + + if rcfile: + cli_args.append("--rcfile") + cli_args.append(rcfile) + + coverage(cli_args) + + +@_named("coverage.xml") +def xml( + *, + rcfile: str | None = None, + data_file: str | None = None, + fail_under: int | None = None, + ignore_errors: bool | None = None, + include: list[str] | None = None, + omit: list[str] | None = None, + output: str | None = None, + quiet: bool | None = None, + skip_empty: bool | None = None, + debug_opts: list[str] | None = None, +) -> None: + """Create an XML report of coverage results. + + Parameters: + rcfile: Specify configuration file. By default `.coveragerc`, `setup.cfg`, `tox.ini`, + and `pyproject.toml` are tried [env: `COVERAGE_RCFILE`]. + data_file: Read coverage data for report generation from this file. + Defaults to `.coverage` [env: `COVERAGE_FILE`]. + fail_under: Exit with a status of 2 if the total coverage is less than the given number. + ignore_errors: Ignore errors while reading source files. + include: Include only files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + omit: Omit files whose paths match one of these patterns. Accepts shell-style wildcards, which must be quoted. + output: Write the JSON report to this file. Defaults to `coverage.json`. + quiet: Don't print messages about what is happening. + skip_empty: Skip files with no code. + debug_opts: Debug options, separated by commas [env: `COVERAGE_DEBUG`]. + """ + cli_args = ["xml"] + + if data_file: + cli_args.append("--data-file") + cli_args.append(data_file) + + if fail_under is not None: + cli_args.append("--fail-under") + cli_args.append(str(fail_under)) + + if ignore_errors: + cli_args.append("--ignore-errors") + + if include: + cli_args.append("--include") + cli_args.append(",".join(include)) + + if omit: + cli_args.append("--omit") + cli_args.append(",".join(omit)) + + if output: + cli_args.append("-o") + cli_args.append(output) + + if quiet: + cli_args.append("--quiet") + + if skip_empty: + cli_args.append("--skip-empty") + + if debug_opts: + cli_args.append("--debug") + cli_args.append(",".join(debug_opts)) + + if rcfile: + cli_args.append("--rcfile") + cli_args.append(rcfile) + + coverage(cli_args) diff --git a/src/duty/callables/flake8.py b/src/duty/callables/flake8.py new file mode 100644 index 0000000..0eecce1 --- /dev/null +++ b/src/duty/callables/flake8.py @@ -0,0 +1,225 @@ +"""Callable for [Flake8](https://github.com/PyCQA/flake8).""" + +from __future__ import annotations + +import sys + +from flake8.main import main as flake8 + +from duty.callables import _named + +# TODO: remove once support for Python 3.7 is dropped +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal + + +@_named("flake8") +def run( + *paths: str, + config: str | None = None, + verbose: bool | None = None, + output_file: str | None = None, + append_config: str | None = None, + isolated: bool | None = None, + enable_extensions: list[str] | None = None, + require_plugins: list[str] | None = None, + quiet: bool | None = None, + color: Literal["auto", "always", "never"] | None = None, + count: bool | None = None, + exclude: list[str] | None = None, + extend_exclude: list[str] | None = None, + filename: list[str] | None = None, + stdin_display_name: str | None = None, + error_format: str | None = None, + hang_closing: bool | None = None, + ignore: list[str] | None = None, + extend_ignore: list[str] | None = None, + per_file_ignores: dict[str, list[str]] | None = None, + max_line_length: int | None = None, + max_doc_length: int | None = None, + indent_size: int | None = None, + select: list[str] | None = None, + extend_select: list[str] | None = None, + disable_noqa: bool | None = None, + show_source: bool | None = None, + no_show_source: bool | None = None, + statistics: bool | None = None, + exit_zero: bool | None = None, + jobs: int | None = None, + tee: bool | None = None, + benchmark: bool | None = None, + bug_report: bool | None = None, +) -> int: + """Run `flake8`. + + Parameters: + *paths: Paths to check. + config: Path to the config file that will be the authoritative config source. + This will cause Flake8 to ignore all other configuration files. + verbose: Print more information about what is happening in flake8. + This option is repeatable and will increase verbosity each time it is repeated. + output_file: Redirect report to a file. + append_config: Provide extra config files to parse in addition to the files found by Flake8 by default. + These files are the last ones read and so they take the highest precedence when multiple files provide the same option. + isolated: Ignore all configuration files. + enable_extensions: Enable plugins and extensions that are otherwise disabled by default. + require_plugins: Require specific plugins to be installed before running. + quiet: Report only file names, or nothing. This option is repeatable. + color: Whether to use color in output. Defaults to `auto`. + count: Print total number of errors to standard output and set the exit code to 1 if total is not empty. + exclude: Comma-separated list of files or directories to exclude (default: ['.svn', 'CVS', '.bzr', '.hg', '.git', '__pycache__', '.tox', '.nox', '.eggs', '*.egg']). + extend_exclude: Comma-separated list of files or directories to add to the list of excluded ones. + filename: Only check for filenames matching the patterns in this comma-separated list (default: ['*.py']). + stdin_display_name: The name used when reporting errors from code passed via stdin. This is useful for editors piping the file contents to flake8 (default: stdin). + error_format: Format errors according to the chosen formatter. + hang_closing: Hang closing bracket instead of matching indentation of opening bracket's line. + ignore: Comma-separated list of error codes to ignore (or skip). For example, ``--ignore=E4,E51,W234`` (default: E121,E123,E126,E226,E24,E704,W503,W504). + extend_ignore: Comma-separated list of error codes to add to the list of ignored ones. For example, ``--extend-ignore=E4,E51,W234``. + per_file_ignores: A pairing of filenames and violation codes that defines which violations to ignore in a particular file. The filenames can be specified in a manner similar to the ``--exclude`` option and the violations work similarly to the ``--ignore`` and ``--select`` options. + max_line_length: Maximum allowed line length for the entirety of this run (default: 79). + max_doc_length: Maximum allowed doc line length for the entirety of this run (default: None). + indent_size: Number of spaces used for indentation (default: 4). + select: Comma-separated list of error codes to enable. For example, ``--select=E4,E51,W234`` (default: E,F,W,C90). + extend_select: Comma-separated list of error codes to add to the list of selected ones. For example, ``--extend-select=E4,E51,W234``. + disable_noqa: Disable the effect of "# noqa". This will report errors on lines with "# noqa" at the end. + show_source: Show the source generate each error or warning. + no_show_source: Negate --show-source. + statistics: Count errors. + exit_zero: Exit with status code "0" even if there are errors. + jobs: Number of subprocesses to use to run checks in parallel. This is ignored on Windows. The default, "auto", will auto-detect the number of processors available to use (default: auto). + tee: Write to stdout and output-file. + benchmark: Print benchmark information about this run of Flake8. + bug_report: Print information necessary when preparing a bug report. + + Returns: + Success/failure. + """ + cli_args = list(paths) + + if verbose: + cli_args.append("--verbose") + + if output_file: + cli_args.append("--output-file") + cli_args.append(output_file) + + if append_config: + cli_args.append("--append-config") + cli_args.append(append_config) + + if config: + cli_args.append("--config") + cli_args.append(config) + + if isolated: + cli_args.append("--isolated") + + if enable_extensions: + cli_args.append("--enable-extensions") + cli_args.append(",".join(enable_extensions)) + + if require_plugins: + cli_args.append("--require-plugins") + cli_args.append(",".join(require_plugins)) + + if quiet: + cli_args.append("--quiet") + + if color: + cli_args.append("--color") + cli_args.append(color) + + if count: + cli_args.append("--count") + + if exclude: + cli_args.append("--exclude") + cli_args.append(",".join(exclude)) + + if extend_exclude: + cli_args.append("--extend-exclude") + cli_args.append(",".join(extend_exclude)) + + if filename: + cli_args.append("--filename") + cli_args.append(",".join(filename)) + + if stdin_display_name: + cli_args.append("--stdin-display-name") + cli_args.append(stdin_display_name) + + if error_format: + cli_args.append("--format") + cli_args.append(error_format) + + if hang_closing: + cli_args.append("--hang-closing") + + if ignore: + cli_args.append("--ignore") + cli_args.append(",".join(ignore)) + + if extend_ignore: + cli_args.append("--extend-ignore") + cli_args.append(",".join(extend_ignore)) + + if per_file_ignores: + cli_args.append("--per-file-ignores") + cli_args.append(" ".join(f"{path}:{','.join(codes)}" for path, codes in per_file_ignores.items())) + + if max_line_length: + cli_args.append("--max-line-length") + cli_args.append(str(max_line_length)) + + if max_doc_length: + cli_args.append("--max-doc-length") + cli_args.append(str(max_doc_length)) + + if indent_size: + cli_args.append("--indent-size") + cli_args.append(str(indent_size)) + + if select: + cli_args.append("--select") + cli_args.append(",".join(select)) + + if extend_select: + cli_args.append("--extend-select") + cli_args.append(",".join(extend_select)) + + if disable_noqa: + cli_args.append("--disable-noqa") + + if show_source: + cli_args.append("--show-source") + + if no_show_source: + cli_args.append("--no-show-source") + + if statistics: + cli_args.append("--statistics") + + if exit_zero: + cli_args.append("--exit-zero") + + if jobs: + cli_args.append("--jobs") + cli_args.append(str(jobs)) + + if tee: + cli_args.append("--tee") + + if benchmark: + cli_args.append("--benchmark") + + if bug_report: + cli_args.append("--bug-report") + + old_sys_argv = sys.argv + sys.argv = ["flake*", *cli_args] + try: + return flake8() + finally: + sys.argv = old_sys_argv diff --git a/src/duty/callables/isort.py b/src/duty/callables/isort.py new file mode 100644 index 0000000..9c30df4 --- /dev/null +++ b/src/duty/callables/isort.py @@ -0,0 +1,578 @@ +"""Callable for [isort](https://github.com/PyCQA/isort).""" + +from __future__ import annotations + +import sys + +from isort.main import main as isort + +from duty.callables import _named + +# TODO: remove once support for Python 3.7 is dropped +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal +Multiline = Literal[ + "GRID", + "VERTICAL", + "HANGING_INDENT", + "VERTICAL_HANGING_INDENT", + "VERTICAL_GRID", + "VERTICAL_GRID_GROUPED", + "VERTICAL_GRID_GROUPED_NO_COMMA", + "NOQA", + "VERTICAL_HANGING_INDENT_BRACKET", + "VERTICAL_PREFIX_FROM_MODULE_IMPORT", + "HANGING_INDENT_WITH_PARENTHESES", + "BACKSLASH_GRID", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", +] + + +@_named("isort") +def run( + *files: str, + settings: str | None = None, + verbose: bool | None = None, + only_modified: bool | None = None, + dedup_headings: bool | None = None, + quiet: bool | None = None, + stdout: bool | None = None, + overwrite_in_place: bool | None = None, + show_config: bool | None = None, + show_files: bool | None = None, + diff: bool | None = None, + check: bool | None = None, + ignore_whitespace: bool | None = None, + config_root: str | None = None, + resolve_all_configs: bool | None = None, + profile: str | None = None, + jobs: int | None = None, + atomic: bool | None = None, + interactive: bool | None = None, + format_error: str | None = None, + format_success: str | None = None, + sort_reexports: bool | None = None, + filter_files: bool | None = None, + skip: list[str] | None = None, + extend_skip: list[str] | None = None, + skip_glob: list[str] | None = None, + extend_skip_glob: list[str] | None = None, + skip_gitignore: bool | None = None, + supported_extension: list[str] | None = None, + blocked_extension: list[str] | None = None, + dont_follow_links: bool | None = None, + filename: str | None = None, + allow_root: bool | None = None, + add_import: str | None = None, + append_only: bool | None = None, + force_adds: bool | None = None, + remove_import: str | None = None, + float_to_top: bool | None = None, + dont_float_to_top: bool | None = None, + combine_as: bool | None = None, + combine_star: bool | None = None, + balanced: bool | None = None, + from_first: bool | None = None, + force_grid_wrap: int | None = None, + indent: str | None = None, + lines_before_imports: int | None = None, + lines_after_imports: int | None = None, + lines_between_types: int | None = None, + line_ending: str | None = None, + length_sort: bool | None = None, + length_sort_straight: bool | None = None, + multi_line: Multiline | None = None, + ensure_newline_before_comments: bool | None = None, + no_inline_sort: bool | None = None, + order_by_type: bool | None = None, + dont_order_by_type: bool | None = None, + reverse_relative: bool | None = None, + reverse_sort: bool | None = None, + sort_order: Literal["natural", "native"] | None = None, + force_single_line_imports: bool | None = None, + single_line_exclusions: list[str] | None = None, + trailing_comma: bool | None = None, + use_parentheses: bool | None = None, + line_length: int | None = None, + wrap_length: int | None = None, + case_sensitive: bool | None = None, + remove_redundant_aliases: bool | None = None, + honor_noqa: bool | None = None, + treat_comment_as_code: str | None = None, + treat_all_comment_as_code: bool | None = None, + formatter: str | None = None, + color: bool | None = None, + ext_format: str | None = None, + star_first: bool | None = None, + split_on_trailing_comma: bool | None = None, + section_default: Literal["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] | None = None, + only_sections: bool | None = None, + no_sections: bool | None = None, + force_alphabetical_sort: bool | None = None, + force_sort_within_sections: bool | None = None, + honor_case_in_force_sorted_sections: bool | None = None, + sort_relative_in_force_sorted_sections: bool | None = None, + force_alphabetical_sort_within_sections: bool | None = None, + top: str | None = None, + combine_straight_imports: bool | None = None, + no_lines_before: list[str] | None = None, + src_path: list[str] | None = None, + builtin: str | None = None, + extra_builtin: str | None = None, + future: str | None = None, + thirdparty: str | None = None, + project: str | None = None, + known_local_folder: str | None = None, + virtual_env: str | None = None, + conda_env: str | None = None, + python_version: Literal["all", "2", "27", "3", "36", "37", "38", "39", "310", "311", "auto"] | None = None, +) -> None: + """Run `isort`. + + Sort Python import definitions alphabetically within logical sections. + Run with no arguments to see a quick start guide, otherwise, one or more files/directories/stdin must be provided. + Use `-` as the first argument to represent stdin. Use --interactive to use the pre 5.0.0 interactive behavior. + If you've used isort 4 but are new to isort 5, see the upgrading guide: + https://pycqa.github.io/isort/docs/upgrade_guides/5.0.0.html. + + Parameters: + *files: One or more Python source files that need their imports sorted. + settings: Explicitly set the settings path or file instead of auto determining based on file location. + verbose: Shows verbose output, such as when files are skipped or when a check is successful. + only_modified: Suppresses verbose output for non-modified files. + dedup_headings: Tells isort to only show an identical custom import heading comment once, even if there are multiple sections with the comment set. + quiet: Shows extra quiet output, only errors are outputted. + stdout: Force resulting output to stdout, instead of in-place. + overwrite_in_place: Tells isort to overwrite in place using the same file handle. Comes at a performance and memory usage penalty over its standard approach but ensures all file flags and modes stay unchanged. + show_config: See isort's determined config, as well as sources of config options. + show_files: See the files isort will be run against with the current config options. + diff: Prints a diff of all the changes isort would make to a file, instead of changing it in place + check: Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. Returns 0 when nothing would change and returns 1 when the file would be reformatted. + ignore_whitespace: Tells isort to ignore whitespace differences when --check-only is being used. + config_root: Explicitly set the config root for resolving all configs. When used with the --resolve-all-configs flag, isort will look at all sub-folders in this config root to resolve config files and sort files based on the closest available config(if any) + resolve_all_configs: Tells isort to resolve the configs for all sub-directories and sort files in terms of its closest config files. + profile: Base profile type to use for configuration. Profiles include: black, django, pycharm, google, open_stack, plone, attrs, hug, wemake, appnexus. As well as any shared profiles. + jobs: Number of files to process in parallel. Negative value means use number of CPUs. + atomic: Ensures the output doesn't save if the resulting file contains syntax errors. + interactive: Tells isort to apply changes interactively. + format_error: Override the format used to print errors. + format_success: Override the format used to print success. + sort_reexports: Automatically sort all re-exports (module level __all__ collections) + filter_files: Tells isort to filter files even when they are explicitly passed in as part of the CLI command. + skip: Files that isort should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. Values can be file names, directory names or file paths. To skip all files in a nested path use --skip-glob. + extend_skip: Extends --skip to add additional files that isort should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. Values can be file names, directory names or file paths. To skip all files in a nested path use --skip-glob. + skip_glob: Files that isort should skip over. + extend_skip_glob: Additional files that isort should skip over (extending --skip-glob). + skip_gitignore: Treat project as a git repository and ignore files listed in .gitignore. NOTE: This requires git to be installed and accessible from the same shell as isort. + supported_extension: Specifies what extensions isort can be run against. + blocked_extension: Specifies what extensions isort can never be run against. + dont_follow_links: Tells isort not to follow symlinks that are encountered when running recursively. + filename: Provide the filename associated with a stream. + allow_root: Tells isort not to treat / specially, allowing it to be run against the root dir. + add_import: Adds the specified import line to all files, automatically determining correct placement. + append_only: Only adds the imports specified in --add-import if the file contains existing imports. + force_adds: Forces import adds even if the original file is empty. + remove_import: Removes the specified import from all files. + float_to_top: Causes all non-indented imports to float to the top of the file having its imports sorted (immediately below the top of file comment). This can be an excellent shortcut for collecting imports every once in a while when you place them in the middle of a file to avoid context switching. *NOTE*: It currently doesn't work with cimports and introduces some extra over-head and a performance penalty. + dont_float_to_top: Forces --float-to-top setting off. See --float-to-top for more information. + combine_as: Combines as imports on the same line. + combine_star: Ensures that if a star import is present, nothing else is imported from that namespace. + balanced: Balances wrapping to produce the most consistent line length possible + from_first: Switches the typical ordering preference, showing from imports first then straight ones. + force_grid_wrap: Force number of from imports (defaults to 2 when passed as CLI flag without value) to be grid wrapped regardless of line length. If 0 is passed in (the global default) only line length is considered. + indent: String to place for indents defaults to " " (4 spaces). + lines_before_imports: Number of lines to insert before imports. + lines_after_imports: Number of lines to insert after imports. + lines_between_types: Number of lines to insert between imports. + line_ending: Forces line endings to the specified value. If not set, values will be guessed per-file. + length_sort: Sort imports by their string length. + length_sort_straight: Sort straight imports by their string length. Similar to `length_sort` but applies only to straight imports and doesn't affect from imports. + multi_line: Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-deprecated-alias-for-5, 7-noqa, 8-vertical-hanging-indent-bracket, 9-vertical-prefix-from- module-import, 10-hanging-indent-with-parentheses). + ensure_newline_before_comments: Inserts a blank line before a comment following an import. + no_inline_sort: Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c ,b`). + order_by_type: Order imports by type, which is determined by case, in addition to alphabetically. **NOTE**: type here refers to the implied type from the import name capitalization. isort does not do type introspection for the imports. These "types" are simply: CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8 or a related coding standard and has many imports this is a good default, otherwise you likely will want to turn it off. From the CLI the `--dont-order-by-type` option will turn this off. + dont_order_by_type: Don't order imports by type, which is determined by case, in addition to alphabetically. **NOTE**: type here refers to the implied type from the import name capitalization. isort does not do type introspection for the imports. These "types" are simply: CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8 or a related coding standard and has many imports this is a good default. You can turn this on from the CLI using `--order-by-type`. + reverse_relative: Reverse order of relative imports. + reverse_sort: Reverses the ordering of imports. + sort_order: Specify sorting function. Can be built in (natural[default] = force numbers to be sequential, native = Python's built-in sorted function) or an installable plugin. + force_single_line_imports: Forces all from imports to appear on their own line + single_line_exclusions EXCLUSIONS: One or more modules to exclude from the single line rule. + trailing_comma: Includes a trailing comma on multi line imports that include parentheses. + use_parentheses: Use parentheses for line continuation on length limit instead of slashes. **NOTE**: This is separate from wrap modes, and only affects how individual lines that are too long get continued, not sections of multiple imports. + line_length: The max length of an import line (used for wrapping long imports). + wrap_length: Specifies how long lines that are wrapped should be, if not set line_length is used. NOTE: wrap_length must be LOWER than or equal to line_length. + case_sensitive: Tells isort to include casing when sorting module names + remove_redundant_aliases: Tells isort to remove redundant aliases from imports, such as `import os as os`. This defaults to `False` simply because some projects use these seemingly useless aliases to signify intent and change behaviour. + honor_noqa: Tells isort to honor noqa comments to enforce skipping those comments. + treat_comment_as_code: Tells isort to treat the specified single line comment(s) as if they are code. + treat_all_comment_as_code: Tells isort to treat all single line comments as if they are code. + formatter: Specifies the name of a formatting plugin to use when producing output. + color: Tells isort to use color in terminal output. + ext_format: Tells isort to format the given files according to an extensions formatting rules. + star_first: Forces star imports above others to avoid overriding directly imported variables. + split_on_trailing_comma: Split imports list followed by a trailing comma into VERTICAL_HANGING_INDENT mode + section_default: Sets the default section for import options: ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') + only_sections: Causes imports to be sorted based on their sections like STDLIB, THIRDPARTY, etc. Within sections, the imports are ordered by their import style and the imports with the same style maintain their relative positions. + no_sections: Put all imports into the same section bucket + force_alphabetical_sort: Force all imports to be sorted as a single section + force_sort_within_sections: Don't sort straight-style imports (like import sys) before from-style imports (like from itertools import groupby). Instead, sort the imports by module, independent of import style. + honor_case_in_force_sorted_sections: Honor `--case-sensitive` when `--force-sort-within-sections` is being used. Without this option set, `--order-by-type` decides module name ordering too. + sort_relative_in_force_sorted_sections: When using `--force-sort-within-sections`, sort relative imports the same way as they are sorted when not using that setting. + force_alphabetical_sort_within_sections: Force all imports to be sorted alphabetically within a section + top: Force specific imports to the top of their appropriate section. + combine_straight_imports: Combines all the bare straight imports of the same section in a single line. Won't work with sections which have 'as' imports + no_lines_before: Sections which should not be split with previous by empty lines + src_path: Add an explicitly defined source path (modules within src paths have their imports automatically categorized as first_party). Glob expansion (`*` and `**`) is supported for this option. + builtin: Force isort to recognize a module as part of Python's standard library. + extra_builtin: Extra modules to be included in the list of ones in Python's standard library. + future: Force isort to recognize a module as part of Python's internal future compatibility libraries. WARNING: this overrides the behavior of __future__ handling and therefore can result in code that can't execute. If you're looking to add dependencies such as six, a better option is to create another section below --future using custom sections. See: https://github.com/PyCQA/isort#custom- sections-and-ordering and the discussion here: https://github.com/PyCQA/isort/issues/1463. + thirdparty: Force isort to recognize a module as being part of a third party library. + project: Force isort to recognize a module as being part of the current python project. + known_local_folder: Force isort to recognize a module as being a local folder. Generally, this is reserved for relative imports (from . import module). + virtual_env: Virtual environment to use for determining whether a package is third-party + conda_env: Conda environment to use for determining whether a package is third-party + python_version: Tells isort to set the known standard library based on the specified Python version. Default is to assume any Python 3 version could be the target, and use a union of all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 311) will be used. + """ + cli_args = list(files) + + if verbose: + cli_args.append("--verbose") + + if only_modified: + cli_args.append("--only-modified") + + if dedup_headings: + cli_args.append("--dedup-headings") + + if quiet: + cli_args.append("--quiet") + + if stdout: + cli_args.append("--stdout") + + if overwrite_in_place: + cli_args.append("--overwrite-in-place") + + if show_config: + cli_args.append("--show-config") + + if show_files: + cli_args.append("--show-files") + + if diff: + cli_args.append("--diff") + + if check: + cli_args.append("--check") + + if ignore_whitespace: + cli_args.append("--ignore-whitespace") + + if settings: + cli_args.append("--settings") + cli_args.append(settings) + + if config_root: + cli_args.append("--config-root") + cli_args.append(config_root) + + if resolve_all_configs: + cli_args.append("--resolve-all-configs") + + if profile: + cli_args.append("--profile") + cli_args.append(profile) + + if jobs: + cli_args.append("--jobs") + cli_args.append(str(jobs)) + + if atomic: + cli_args.append("--atomic") + + if interactive: + cli_args.append("--interactive") + + if format_error: + cli_args.append("--format-error") + cli_args.append(format_error) + + if format_success: + cli_args.append("--format-success") + cli_args.append(format_success) + + if sort_reexports: + cli_args.append("--sort-reexports") + + if filter_files: + cli_args.append("--filter-files") + + if skip: + cli_args.append("--skip") + cli_args.append(",".join(skip)) + + if extend_skip: + cli_args.append("--extend-skip") + cli_args.append(",".join(extend_skip)) + + if skip_glob: + cli_args.append("--skip-glob") + cli_args.append(",".join(skip_glob)) + + if extend_skip_glob: + cli_args.append("--extend-skip-glob") + cli_args.append(",".join(extend_skip_glob)) + + if skip_gitignore: + cli_args.append("--skip-gitignore") + + if supported_extension: + cli_args.append("--supported-extension") + cli_args.append(",".join(supported_extension)) + + if blocked_extension: + cli_args.append("--blocked-extension") + cli_args.append(",".join(blocked_extension)) + + if dont_follow_links: + cli_args.append("--dont-follow-links") + + if filename: + cli_args.append("--filename") + cli_args.append(filename) + + if allow_root: + cli_args.append("--allow-root") + + if add_import: + cli_args.append("--add-import") + cli_args.append(add_import) + + if append_only: + cli_args.append("--append-only") + + if force_adds: + cli_args.append("--force-adds") + + if remove_import: + cli_args.append("--remove-import") + cli_args.append(remove_import) + + if float_to_top: + cli_args.append("--float-to-top") + + if dont_float_to_top: + cli_args.append("--dont-float-to-top") + + if combine_as: + cli_args.append("--combine-as") + + if combine_star: + cli_args.append("--combine-star") + + if balanced: + cli_args.append("--balanced") + + if from_first: + cli_args.append("--from-first") + + if force_grid_wrap: + cli_args.append("--force-grid-wrap") + cli_args.append(str(force_grid_wrap)) + + if indent: + cli_args.append("--indent") + cli_args.append(indent) + + if lines_before_imports: + cli_args.append("--lines-before-imports") + cli_args.append(str(lines_before_imports)) + + if lines_after_imports: + cli_args.append("--lines-after-imports") + cli_args.append(str(lines_after_imports)) + + if lines_between_types: + cli_args.append("--lines-between-types") + cli_args.append(str(lines_between_types)) + + if line_ending: + cli_args.append("--line-ending") + cli_args.append(line_ending) + + if length_sort: + cli_args.append("--length-sort") + + if length_sort_straight: + cli_args.append("--length-sort-straight") + + if multi_line: + cli_args.append("--multi-line") + cli_args.append(multi_line) + + if ensure_newline_before_comments: + cli_args.append("--ensure-newline-before-comments") + + if no_inline_sort: + cli_args.append("--no-inline-sort") + + if order_by_type: + cli_args.append("--order-by-type") + + if dont_order_by_type: + cli_args.append("--dont-order-by-type") + + if reverse_relative: + cli_args.append("--reverse-relative") + + if reverse_sort: + cli_args.append("--reverse-sort") + + if sort_order: + cli_args.append("--sort-order") + cli_args.append(sort_order) + + if force_single_line_imports: + cli_args.append("--force-single-line-imports") + + if single_line_exclusions: + cli_args.append("--single-line-exclusions") + cli_args.append(",".join(single_line_exclusions)) + + if trailing_comma: + cli_args.append("--trailing-comma") + + if use_parentheses: + cli_args.append("--use-parentheses") + + if line_length: + cli_args.append("--line-length") + cli_args.append(str(line_length)) + + if wrap_length: + cli_args.append("--wrap-length") + cli_args.append(str(wrap_length)) + + if case_sensitive: + cli_args.append("--case-sensitive") + + if remove_redundant_aliases: + cli_args.append("--remove-redundant-aliases") + + if honor_noqa: + cli_args.append("--honor-noqa") + + if treat_comment_as_code: + cli_args.append("--treat-comment-as-code") + cli_args.append(treat_comment_as_code) + + if treat_all_comment_as_code: + cli_args.append("--treat-all-comment-as-code") + + if formatter: + cli_args.append("--formatter") + cli_args.append(formatter) + + if color: + cli_args.append("--color") + + if ext_format: + cli_args.append("--ext-format") + cli_args.append(ext_format) + + if star_first: + cli_args.append("--star-first") + + if split_on_trailing_comma: + cli_args.append("--split-on-trailing-comma") + + if section_default: + cli_args.append("--section-default") + cli_args.append(section_default) + + if only_sections: + cli_args.append("--only-sections") + + if no_sections: + cli_args.append("--no-sections") + + if force_alphabetical_sort: + cli_args.append("--force-alphabetical-sort") + + if force_sort_within_sections: + cli_args.append("--force-sort-within-sections") + + if honor_case_in_force_sorted_sections: + cli_args.append("--honor-case-in-force-sorted-sections") + + if sort_relative_in_force_sorted_sections: + cli_args.append("--sort-relative-in-force-sorted-sections") + + if force_alphabetical_sort_within_sections: + cli_args.append("force-alphabetical-sort-within-sections") + + if top: + cli_args.append("--top") + cli_args.append(top) + + if combine_straight_imports: + cli_args.append("--combine-straight-imports") + + if no_lines_before: + cli_args.append("--no-lines-before") + cli_args.append(",".join(no_lines_before)) + + if src_path: + cli_args.append("--src-path") + cli_args.append(",".join(src_path)) + + if builtin: + cli_args.append("--builtin") + cli_args.append(builtin) + + if extra_builtin: + cli_args.append("--extra-builtin") + cli_args.append(extra_builtin) + + if future: + cli_args.append("--future") + cli_args.append(future) + + if thirdparty: + cli_args.append("--thirdparty") + cli_args.append(thirdparty) + + if project: + cli_args.append("--project") + cli_args.append(project) + + if known_local_folder: + cli_args.append("--known-local-folder") + cli_args.append(known_local_folder) + + if virtual_env: + cli_args.append("--virtual-env") + cli_args.append(virtual_env) + + if conda_env: + cli_args.append("--conda-env") + cli_args.append(conda_env) + + if python_version: + cli_args.append("--python-version") + cli_args.append(python_version) + + isort(cli_args) diff --git a/src/duty/callables/mkdocs.py b/src/duty/callables/mkdocs.py new file mode 100644 index 0000000..31243e8 --- /dev/null +++ b/src/duty/callables/mkdocs.py @@ -0,0 +1,256 @@ +"""Callable for [MkDocs](https://github.com/mkdocs/mkdocs).""" + +from __future__ import annotations + +from mkdocs.__main__ import cli as mkdocs + +from duty.callables import _named + + +def run(*args: str, quiet: bool = False, verbose: bool = False) -> None: + """Run `mkdocs`. + + Parameters: + *args: CLI arguments. + quiet: Silence warnings. + verbose: Enable verbose output. + """ + cli_args = list(args) + + if quiet and "-q" not in cli_args: + cli_args.append("--quiet") + + if verbose and "-v" not in cli_args: + cli_args.append("--verbose") + + mkdocs(cli_args) + + +@_named("mkdocs.build") +def build( + *, + config_file: str | None = None, + clean: bool | None = None, + strict: bool | None = None, + theme: str | None = None, + directory_urls: bool | None = None, + site_dir: str | None = None, + quiet: bool = False, + verbose: bool = False, +) -> None: + """Build the MkDocs documentation. + + Parameters: + config_file: Provide a specific MkDocs config. + clean: Remove old files from the site_dir before building (the default). + strict: Enable strict mode. This will cause MkDocs to abort the build on any warnings. + theme: The theme to use when building your documentation. + directory_urls: Use directory URLs when building pages (the default). + site_dir: The directory to output the result of the documentation build. + quiet: Silence warnings. + verbose: Enable verbose output. + """ + cli_args = [] + + if clean is True: + cli_args.append("--clean") + elif clean is False: + cli_args.append("--dirty") + + if config_file: + cli_args.append("--config-file") + cli_args.append(config_file) + + if strict is True: + cli_args.append("--strict") + + if theme: + cli_args.append("--theme") + cli_args.append(theme) + + if directory_urls is True: + cli_args.append("--use-directory-urls") + elif directory_urls is False: + cli_args.append("--no-directory-urls") + + if site_dir: + cli_args.append("--site_dir") + cli_args.append(site_dir) + + run("build", *cli_args, quiet=quiet, verbose=verbose) + + +@_named("mkdocs.gh_deploy") +def gh_deploy( + *, + config_file: str | None = None, + clean: bool | None = None, + message: str | None = None, + remote_branch: str | None = None, + remote_name: str | None = None, + force: bool | None = None, + no_history: bool | None = None, + ignore_version: bool | None = None, + shell: bool | None = None, + strict: bool | None = None, + theme: str | None = None, + directory_urls: bool | None = None, + site_dir: str | None = None, + quiet: bool = False, + verbose: bool = False, +) -> None: + """Deploy your documentation to GitHub Pages. + + Parameters: + config_file: Provide a specific MkDocs config. + clean: Remove old files from the site_dir before building (the default). + message: A commit message to use when committing to the GitHub Pages remote branch. + Commit {sha} and MkDocs {version} are available as expansions. + remote_branch: The remote branch to commit to for GitHub Pages. This overrides the value specified in config. + remote_name: The remote name to commit to for GitHub Pages. This overrides the value specified in config + force: Force the push to the repository. + no_history: Replace the whole Git history with one new commit. + ignore_version: Ignore check that build is not being deployed with an older version of MkDocs. + shell: Use the shell when invoking Git. + strict: Enable strict mode. This will cause MkDocs to abort the build on any warnings. + theme: The theme to use when building your documentation. + directory_urls: Use directory URLs when building pages (the default). + site_dir: The directory to output the result of the documentation build. + quiet: Silence warnings. + verbose: Enable verbose output. + """ + cli_args = [] + + if clean is True: + cli_args.append("--clean") + elif clean is False: + cli_args.append("--dirty") + + if message: + cli_args.append("--message") + cli_args.append(message) + + if remote_branch: + cli_args.append("--remote-branch") + cli_args.append(remote_branch) + + if remote_name: + cli_args.append("--remote-name") + cli_args.append(remote_name) + + if force: + cli_args.append("--force") + + if no_history: + cli_args.append("--no-history") + + if ignore_version: + cli_args.append("--ignore-version") + + if shell: + cli_args.append("--shell") + + if config_file: + cli_args.append("--config-file") + cli_args.append(config_file) + + if strict is True: + cli_args.append("--strict") + + if theme: + cli_args.append("--theme") + cli_args.append(theme) + + if directory_urls is True: + cli_args.append("--use-directory-urls") + elif directory_urls is False: + cli_args.append("--no-directory-urls") + + if site_dir: + cli_args.append("--site_dir") + cli_args.append(site_dir) + + run("gh-deploy", *cli_args, quiet=quiet, verbose=verbose) + + +@_named("mkdocs.new") +def new(project_directory: str, *, quiet: bool = False, verbose: bool = False) -> None: + """Create a new MkDocs project. + + Parameters: + project_directory: Where to create the project. + quiet: Silence warnings. + verbose: Enable verbose output. + """ + run("new", project_directory, quiet=quiet, verbose=verbose) + + +@_named("mkdocs.serve") +def serve( + *, + config_file: str | None = None, + dev_addr: str | None = None, + livereload: bool | None = None, + dirtyreload: bool | None = None, + watch_theme: bool | None = None, + watch: list[str] | None = None, + strict: bool | None = None, + theme: str | None = None, + directory_urls: bool | None = None, + quiet: bool = False, + verbose: bool = False, +) -> None: + """Run the builtin development server. + + Parameters: + config_file: Provide a specific MkDocs config. + dev_addr: IP address and port to serve documentation locally (default: localhost:8000). + livereload: Enable/disable the live reloading in the development server. + dirtyreload: nable the live reloading in the development server, but only re-build files that have changed. + watch_theme: Include the theme in list of files to watch for live reloading. Ignored when live reload is not used. + watch: Directories or files to watch for live reloading. + strict: Enable strict mode. This will cause MkDocs to abort the build on any warnings. + theme: The theme to use when building your documentation. + directory_urls: Use directory URLs when building pages (the default). + quiet: Silence warnings. + verbose: Enable verbose output. + """ + cli_args = [] + + if dev_addr: + cli_args.append("--dev-addr") + cli_args.append(dev_addr) + + if livereload is True: + cli_args.append("--livereload") + elif livereload is False: + cli_args.append("--no-livereload") + + if dirtyreload: + cli_args.append("--dirtyreload") + + if watch_theme: + cli_args.append("--watch-theme") + + if watch: + for path in watch: + cli_args.append("--watch") + cli_args.append(path) + + if config_file: + cli_args.append("--config-file") + cli_args.append(config_file) + + if strict is True: + cli_args.append("--strict") + + if theme: + cli_args.append("--theme") + cli_args.append(theme) + + if directory_urls is True: + cli_args.append("--use-directory-urls") + elif directory_urls is False: + cli_args.append("--no-directory-urls") + + run("serve", *cli_args, quiet=quiet, verbose=verbose) diff --git a/src/duty/callables/mypy.py b/src/duty/callables/mypy.py new file mode 100644 index 0000000..578de7c --- /dev/null +++ b/src/duty/callables/mypy.py @@ -0,0 +1,501 @@ +"""Callable for [Mypy](https://github.com/python/mypy).""" + +from __future__ import annotations + +import sys + +from mypy.main import main as mypy + +from duty.callables import _named +from duty.callables._io import _LazyStderr, _LazyStdout + +# TODO: remove once support for Python 3.7 is dropped +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal + + +@_named("mypy") +def run( + *paths: str, + config_file: str | None = None, + enable_incomplete_feature: bool | None = None, + verbose: bool | None = None, + warn_unused_configs: bool | None = None, + no_namespace_packages: bool | None = None, + ignore_missing_imports: bool | None = None, + follow_imports: Literal["normal", "silent", "skip", "error"] | None = None, + python_executable: str | None = None, + no_site_packages: bool | None = None, + no_silence_site_packages: bool | None = None, + python_version: str | None = None, + py2: bool | None = None, + platform: str | None = None, + always_true: list[str] | None = None, + always_false: list[str] | None = None, + disallow_any_unimported: bool | None = None, + disallow_any_expr: bool | None = None, + disallow_any_decorated: bool | None = None, + disallow_any_explicit: bool | None = None, + disallow_any_generics: bool | None = None, + disallow_subclassing_any: bool | None = None, + disallow_untyped_calls: bool | None = None, + disallow_untyped_defs: bool | None = None, + disallow_incomplete_defs: bool | None = None, + check_untyped_defs: bool | None = None, + disallow_untyped_decorators: bool | None = None, + implicit_optional: bool | None = None, + no_strict_optional: bool | None = None, + warn_redundant_casts: bool | None = None, + warn_unused_ignores: bool | None = None, + no_warn_no_return: bool | None = None, + warn_return_any: bool | None = None, + warn_unreachable: bool | None = None, + allow_untyped_globals: bool | None = None, + allow_redefinition: bool | None = None, + no_implicit_reexport: bool | None = None, + strict_equality: bool | None = None, + strict_concatenate: bool | None = None, + strict: bool | None = None, + disable_error_code: str | None = None, + enable_error_code: str | None = None, + show_error_context: bool | None = None, + show_column_numbers: bool | None = None, + show_error_end: bool | None = None, + hide_error_codes: bool | None = None, + pretty: bool | None = None, + no_color_output: bool | None = None, + no_error_summary: bool | None = None, + show_absolute_path: bool | None = None, + no_incremental: bool | None = None, + cache_dir: str | None = None, + sqlite_cache: bool | None = None, + cache_fine_grained: bool | None = None, + skip_version_check: bool | None = None, + skip_cache_mtime_checks: bool | None = None, + pdb: bool | None = None, + show_traceback: bool | None = None, + raise_exceptions: bool | None = None, + custom_typing_module: str | None = None, + disable_recursive_aliases: bool | None = None, + custom_typeshed_dir: str | None = None, + warn_incomplete_stub: bool | None = None, + shadow_file: tuple[str, str] | None = None, + any_exprs_report: str | None = None, + cobertura_xml_report: str | None = None, + html_report: str | None = None, + linecount_report: str | None = None, + linecoverage_report: str | None = None, + lineprecision_report: str | None = None, + txt_report: str | None = None, + xml_report: str | None = None, + xslt_html_report: str | None = None, + xslt_txt_report: str | None = None, + junit_xml: str | None = None, + find_occurrences: str | None = None, + scripts_are_modules: bool | None = None, + install_types: bool | None = None, + non_interactive: bool | None = None, + explicit_package_bases: bool | None = None, + exclude: str | None = None, + module: str | None = None, + package: str | None = None, + command: str | None = None, +) -> None: + r"""Run mypy. + + Parameters: + *paths: Path to scan. + config_file: Configuration file, must have a [mypy] section (defaults to mypy.ini, .mypy.ini, + enable_incomplete_feature: Enable support of incomplete/experimental features for early preview. + verbose: More verbose messages. + pyproject.toml, setup.cfg, /home/pawamoy/.config/mypy/config, ~/.config/mypy/config, ~/.mypy.ini). + warn_unused_configs: Warn about unused '[mypy-]' or '[[tool.mypy.overrides]]' config sections + (inverse: --no-warn-unused-configs). + no_namespace_packages: Support namespace packages (PEP 420, __init__.py-less) (inverse: --namespace-packages). + ignore_missing_imports: Silently ignore imports of missing modules. + follow_imports: How to treat imports (default normal). + python_executable: Python executable used for finding PEP 561 compliant installed packages and stubs. + no_site_packages: Do not search for installed PEP 561 compliant packages. + no_silence_site_packages: Do not silence errors in PEP 561 compliant installed packages. + python_version: Type check code assuming it will be running on Python x.y. + py2: Use Python 2 mode (same as --python-version 2.7). + platform: Type check special-cased code for the given OS platform (defaults to sys.platform). + always_true: Additional variable to be considered True (may be repeated). + always_false: Additional variable to be considered False (may be repeated). + disallow_any_unimported: Disallow Any types resulting from unfollowed imports. + disallow_any_expr: Disallow all expressions that have type Any. + disallow_any_decorated: Disallow functions that have Any in their signature after decorator transformation. + disallow_any_explicit: Disallow explicit Any in type positions. + disallow_any_generics: Disallow usage of generic types that do not specify explicit type parameters + (inverse: --allow-any-generics). + disallow_subclassing_any: Disallow subclassing values of type 'Any' when defining classes + (inverse: --allow-subclassing-any). + disallow_untyped_calls: Disallow calling functions without type annotations from functions with type annotations + (inverse: --allow-untyped-calls). + disallow_untyped_defs: Disallow defining functions without type annotations or with incomplete type annotations + (inverse: --allow-untyped-defs). + disallow_incomplete_defs: Disallow defining functions with incomplete type annotations + (inverse: --allow-incomplete-defs). + check_untyped_defs: Type check the interior of functions without type annotations + (inverse: --no-check-untyped-defs). + disallow_untyped_decorators: Disallow decorating typed functions with untyped decorators + (inverse: --allow-untyped-decorators). + implicit_optional: Assume arguments with default values of None are Optional(inverse: --no-implicit-optional). + no_strict_optional: Disable strict Optional checks (inverse: --strict-optional). + warn_redundant_casts: Warn about casting an expression to its inferred type (inverse: --no-warn-redundant-casts). + warn_unused_ignores: Warn about unneeded '# type: ignore' comments (inverse: --no-warn-unused-ignores). + no_warn_no_return: Do not warn about functions that end without returning (inverse: --warn-no-return). + warn_return_any: Warn about returning values of type Any from non-Any typed functions (inverse: --no-warn-return-any). + warn_unreachable: Warn about statements or expressions inferred to be unreachable (inverse: --no-warn-unreachable). + allow_untyped_globals: Suppress toplevel errors caused by missing annotations (inverse: --disallow-untyped-globals). + allow_redefinition: Allow unconditional variable redefinition with a new type (inverse: --disallow-redefinition). + no_implicit_reexport: Treat imports as private unless aliased (inverse: --implicit-reexport). + strict_equality: Prohibit equality, identity, and container checks for non-overlapping types + (inverse: --no-strict-equality). + strict_concatenate: Make arguments prepended via Concatenate be truly positional-only (inverse: --no-strict-concatenate). + strict: Strict mode; enables the following flags: --warn-unused-configs, --disallow-any-generics, + --disallow-subclassing-any, --disallow-untyped-calls, --disallow-untyped-defs, --disallow-incomplete-defs, + --check-untyped-defs, --disallow-untyped-decorators, --warn-redundant-casts, --warn-unused-ignores, + --warn-return-any, --no-implicit-reexport, --strict-equality, --strict-concatenate. + disable_error_code: Disable a specific error code. + enable_error_code: Enable a specific error code. + show_error_context: Precede errors with "note:" messages explaining context (inverse: --hide-error-context). + show_column_numbers: Show column numbers in error messages (inverse: --hide-column-numbers). + show_error_end: Show end line/end column numbers in error messages. This implies --show-column-numbers + (inverse: --hide-error-end). + hide_error_codes: Hide error codes in error messages (inverse: --show-error-codes). + pretty: Use visually nicer output in error messages: Use soft word wrap, show source code snippets, + and show error location markers (inverse: --no-pretty). + no_color_output: Do not colorize error messages (inverse: --color-output). + no_error_summary: Do not show error stats summary (inverse: --error-summary). + show_absolute_path: Show absolute paths to files (inverse: --hide-absolute-path). + no_incremental: Disable module cache (inverse: --incremental). + cache_dir: Store module cache info in the given folder in incremental mode (defaults to '.mypy_cache'). + sqlite_cache: Use a sqlite database to store the cache (inverse: --no-sqlite-cache). + cache_fine_grained: Include fine-grained dependency information in the cache for the mypy daemon. + skip_version_check: Allow using cache written by older mypy version. + skip_cache_mtime_checks: Skip cache internal consistency checks based on mtime. + pdb: Invoke pdb on fatal error. + show_traceback: Show traceback on fatal error. + raise_exceptions: Raise exception on fatal error. + custom_typing_module: Use a custom typing module. + disable_recursive_aliases: Disable experimental support for recursive type aliases. + custom_typeshed_dir: Use the custom typeshed in DIR. + warn_incomplete_stub: Warn if missing type annotation in typeshed, only relevant with --disallow-untyped-defs + or --disallow-incomplete-defs enabled (inverse: --no-warn-incomplete-stub). + shadow_file: When encountering SOURCE_FILE, read and type check the contents of SHADOW_FILE instead.. + any_exprs_report: Report any expression. + cobertura_xml_report: Report Cobertura. + html_report: Report HTML. + linecount_report: Report line count. + linecoverage_report: Report line coverage. + lineprecision_report: Report line precision. + txt_report: Report text. + xml_report: Report XML. + xslt_html_report: Report XLST HTML. + xslt_txt_report: Report XLST text. + junit_xml: Write junit.xml to the given file. + find_occurrences: Print out all usages of a class member (experimental). + scripts_are_modules: Script x becomes module x instead of __main__. + install_types: Install detected missing library stub packages using pip (inverse: --no-install-types). + non_interactive: Install stubs without asking for confirmation and hide errors, with --install-types + (inverse: --interactive). + explicit_package_bases: Use current directory and MYPYPATH to determine module names of files passed + (inverse: --no-explicit-package-bases). + exclude: Regular expression to match file names, directory names or paths which mypy should ignore while + recursively discovering files to check, e.g. --exclude '/setup\.py$'. + May be specified more than once, eg. --exclude a --exclude b. + module: Type-check module; can repeat for more modules. + package: Type-check package recursively; can be repeated. + command: Type-check program passed in as string. + """ + cli_args = list(paths) + + if enable_incomplete_feature: + cli_args.append("--enable-incomplete-feature") + + if verbose: + cli_args.append("--verbose") + + if config_file: + cli_args.append("--config-file") + cli_args.append(config_file) + + if warn_unused_configs: + cli_args.append("--warn-unused-configs") + + if no_namespace_packages: + cli_args.append("--no-namespace-packages") + + if ignore_missing_imports: + cli_args.append("--ignore-missing-imports") + + if follow_imports: + cli_args.append("--follow-imports") + cli_args.append(follow_imports) + + if python_executable: + cli_args.append("--python-executable") + cli_args.append(python_executable) + + if no_site_packages: + cli_args.append("--no-site-packages") + + if no_silence_site_packages: + cli_args.append("--no-silence-site-packages") + + if python_version: + cli_args.append("--python-version") + cli_args.append(python_version) + + if py2: + cli_args.append("--py2") + + if platform: + cli_args.append("--platform") + cli_args.append(platform) + + if always_true: + for posarg in always_true: + cli_args.append("--always-true") + cli_args.append(posarg) + + if always_false: + for posarg in always_false: + cli_args.append("--always-false") + cli_args.append(posarg) + + if disallow_any_unimported: + cli_args.append("--disallow-any-unimported") + + if disallow_any_expr: + cli_args.append("--disallow-any-expr") + + if disallow_any_decorated: + cli_args.append("--disallow-any-decorated") + + if disallow_any_explicit: + cli_args.append("--disallow-any-explicit") + + if disallow_any_generics: + cli_args.append("--disallow-any-generics") + + if disallow_subclassing_any: + cli_args.append("--disallow-subclassing-any") + + if disallow_untyped_calls: + cli_args.append("--disallow-untyped-calls") + + if disallow_untyped_defs: + cli_args.append("--disallow-untyped-defs") + + if disallow_incomplete_defs: + cli_args.append("--disallow-incomplete-defs") + + if check_untyped_defs: + cli_args.append("--check-untyped-defs") + + if disallow_untyped_decorators: + cli_args.append("--disallow-untyped-decorators") + + if implicit_optional: + cli_args.append("--implicit-optional") + + if no_strict_optional: + cli_args.append("--no-strict-optional") + + if warn_redundant_casts: + cli_args.append("--warn-redundant-casts") + + if warn_unused_ignores: + cli_args.append("--warn-unused-ignores") + + if no_warn_no_return: + cli_args.append("--no-warn-no-return") + + if warn_return_any: + cli_args.append("--warn-return-any") + + if warn_unreachable: + cli_args.append("--warn-unreachable") + + if allow_untyped_globals: + cli_args.append("--allow-untyped-globals") + + if allow_redefinition: + cli_args.append("--allow-redefinition") + + if no_implicit_reexport: + cli_args.append("--no-implicit-reexport") + + if strict_equality: + cli_args.append("--strict-equality") + + if strict_concatenate: + cli_args.append("--strict-concatenate") + + if strict: + cli_args.append("--strict") + + if disable_error_code: + cli_args.append("--disable-error-code") + cli_args.append(disable_error_code) + + if enable_error_code: + cli_args.append("--enable-error-code") + cli_args.append(enable_error_code) + + if show_error_context: + cli_args.append("--show-error-context") + + if show_column_numbers: + cli_args.append("--show-column-numbers") + + if show_error_end: + cli_args.append("--show-error-end") + + if hide_error_codes: + cli_args.append("--hide-error-codes") + + if pretty: + cli_args.append("--pretty") + + if no_color_output: + cli_args.append("--no-color-output") + + if no_error_summary: + cli_args.append("--no-error-summary") + + if show_absolute_path: + cli_args.append("--show-absolute-path") + + if no_incremental: + cli_args.append("--no-incremental") + + if cache_dir: + cli_args.append("--cache-dir") + cli_args.append(cache_dir) + + if sqlite_cache: + cli_args.append("--sqlite-cache") + + if cache_fine_grained: + cli_args.append("--cache-fine-grained") + + if skip_version_check: + cli_args.append("--skip-version-check") + + if skip_cache_mtime_checks: + cli_args.append("--skip-cache-mtime-checks") + + if pdb: + cli_args.append("--pdb") + + if show_traceback: + cli_args.append("--show-traceback") + + if raise_exceptions: + cli_args.append("--raise-exceptions") + + if custom_typing_module: + cli_args.append("--custom-typing-module") + cli_args.append(custom_typing_module) + + if disable_recursive_aliases: + cli_args.append("--disable-recursive-aliases") + + if custom_typeshed_dir: + cli_args.append("--custom-typeshed-dir") + cli_args.append(custom_typeshed_dir) + + if warn_incomplete_stub: + cli_args.append("--warn-incomplete-stub") + + if shadow_file: + cli_args.append("--shadow-file") + cli_args.extend(shadow_file) + + if any_exprs_report: + cli_args.append("--any-exprs-report") + cli_args.append(any_exprs_report) + + if cobertura_xml_report: + cli_args.append("--cobertura-xml-report") + cli_args.append(cobertura_xml_report) + + if html_report: + cli_args.append("--html-report") + cli_args.append(html_report) + + if linecount_report: + cli_args.append("--linecount-report") + cli_args.append(linecount_report) + + if linecoverage_report: + cli_args.append("--linecoverage-report") + cli_args.append(linecoverage_report) + + if lineprecision_report: + cli_args.append("--lineprecision-report") + cli_args.append(lineprecision_report) + + if txt_report: + cli_args.append("--txt-report") + cli_args.append(txt_report) + + if xml_report: + cli_args.append("--xml-report") + cli_args.append(xml_report) + + if xslt_html_report: + cli_args.append("--xslt-html-report") + cli_args.append(xslt_html_report) + + if xslt_txt_report: + cli_args.append("--xslt-txt-report") + cli_args.append(xslt_txt_report) + + if junit_xml: + cli_args.append("--junit-xml") + cli_args.append(junit_xml) + + if find_occurrences: + cli_args.append("--find-occurrences") + cli_args.append(find_occurrences) + + if scripts_are_modules: + cli_args.append("--scripts-are-modules") + + if install_types: + cli_args.append("--install-types") + + if non_interactive: + cli_args.append("--non-interactive") + + if explicit_package_bases: + cli_args.append("--explicit-package-bases") + + if exclude: + cli_args.append("--exclude") + cli_args.append(exclude) + + if module: + cli_args.append("--module") + cli_args.append(module) + + if package: + cli_args.append("--package") + cli_args.append(package) + + if command: + cli_args.append("--command") + cli_args.append(command) + + mypy( + args=cli_args, + stdout=_LazyStdout(), + stderr=_LazyStderr(), + clean_exit=True, + ) diff --git a/src/duty/callables/pytest.py b/src/duty/callables/pytest.py new file mode 100644 index 0000000..282550c --- /dev/null +++ b/src/duty/callables/pytest.py @@ -0,0 +1,481 @@ +"""Callable for [pytest](https://github.com/pytest-dev/pytest).""" + +from __future__ import annotations + +import sys + +from pytest import main as pytest # noqa: PT013 + +from duty.callables import _named + +# TODO: remove once support for Python 3.7 is dropped +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal + + +@_named("pytest") +def run( + *paths: str, + config_file: str | None = None, + select: str | None = None, + select_markers: str | None = None, + markers: bool | None = None, + exitfirst: bool | None = None, + fixtures: bool | None = None, + fixtures_per_test: bool | None = None, + pdb: bool | None = None, + pdbcls: str | None = None, + trace: bool | None = None, + capture: str | None = None, + runxfail: bool | None = None, + last_failed: bool | None = None, + failed_first: bool | None = None, + new_first: bool | None = None, + cache_show: str | None = None, + cache_clear: bool | None = None, + last_failed_no_failures: Literal["all", "none"] | None = None, + stepwise: bool | None = None, + stepwise_skip: bool | None = None, + durations: int | None = None, + durations_min: int | None = None, + verbose: bool | None = None, + no_header: bool | None = None, + no_summary: bool | None = None, + quiet: bool | None = None, + verbosity: int | None = None, + show_extra_summary: str | None = None, + disable_pytest_warnings: bool | None = None, + showlocals: bool | None = None, + no_showlocals: bool | None = None, + traceback: Literal["auto", "long", "short", "line", "native", "no"] | None = None, + show_capture: Literal["no", "stdout", "stderr", "log", "all"] | None = None, + full_trace: bool | None = None, + color: str | None = None, + code_highlight: bool | None = None, + pastebin: str | None = None, + junit_xml: str | None = None, + junit_prefix: str | None = None, + pythonwarnings: str | None = None, + maxfail: int | None = None, + strict_config: bool | None = None, + strict_markers: bool | None = None, + continue_on_collection_errors: bool | None = None, + rootdir: str | None = None, + collect_only: bool | None = None, + pyargs: bool | None = None, + ignore: list[str] | None = None, + ignore_glob: list[str] | None = None, + deselect: str | None = None, + confcutdir: str | None = None, + noconftest: bool | None = None, + keep_duplicates: bool | None = None, + collect_in_virtualenv: bool | None = None, + import_mode: Literal["prepend", "append", "importlib"] | None = None, + doctest_modules: bool | None = None, + doctest_report: Literal["none", "cdiff", "ndiff", "udiff", "only_first_failure"] | None = None, + doctest_glob: str | None = None, + doctest_ignore_import_errors: bool | None = None, + doctest_continue_on_failure: bool | None = None, + basetemp: str | None = None, + plugins: list[str] | None = None, + no_plugins: list[str] | None = None, + trace_config: bool | None = None, + debug: str | None = None, + override_ini: str | None = None, + assert_mode: str | None = None, + setup_only: bool | None = None, + setup_show: bool | None = None, + setup_plan: bool | None = None, + log_level: str | None = None, + log_format: str | None = None, + log_date_format: str | None = None, + log_cli_level: tuple[str, str] | None = None, + log_cli_format: str | None = None, + log_cli_date_format: str | None = None, + log_file: str | None = None, + log_file_level: str | None = None, + log_file_format: str | None = None, + log_file_date_format: str | None = None, + log_auto_indent: str | None = None, +) -> int: + """Run `pytest`. + + Parameters: + *paths: Files or directories to select tests from. + select: Only run tests which match the given substring expression. An expression is a Python evaluatable expression where all names are substring-matched against test names and their parent classes. Example: -k 'test_method or test_other' matches all test functions and classes whose name contains 'test_method' or 'test_other', while -k 'not test_method' matches those that don't contain 'test_method' in their names. -k 'not test_method and not test_other' will eliminate the matches. Additionally keywords are matched to classes and functions containing extra names in their 'extra_keyword_matches' set, as well as functions which have names assigned directly to them. The matching is case-insensitive. + select_markers: Only run tests matching given mark expression. For example: -m 'mark1 and not mark2'. + markers: show markers (builtin, plugin and per-project ones). + exitfirst: Exit instantly on first error or failed test + fixtures: Show available fixtures, sorted by plugin appearance (fixtures with leading '_' are only shown with '-v') + fixtures_per_test: Show fixtures per test + pdb: Start the interactive Python debugger on errors or KeyboardInterrupt + pdbcls: Specify a custom interactive Python debugger for use with --pdb.For example: --pdbcls IPython.terminal.debugger:TerminalPdb + trace: Immediately break when running each test + capture: Per-test capturing method: one of fd|sys|no|tee-sys + runxfail: Report the results of xfail tests as if they were not marked + last_failed: Rerun only the tests that failed at the last run (or all if none failed) + failed_first: Run all tests, but run the last failures first. This may re-order tests and thus lead to repeated fixture setup/teardown. + new_first: Run tests from new files first, then the rest of the tests sorted by file mtime + cache_show: Show cache contents, don't perform collection or tests. Optional argument: glob (default: '*'). + cache_clear: Remove all cache contents at start of test run + last_failed_no_failures: Which tests to run with no previously (known) failures + stepwise: Exit on test failure and continue from last failing test next time + stepwise_skip: Ignore the first failing test but stop on the next failing test. Implicitly enables --stepwise. + durations: Show N slowest setup/test durations (N 0 for all) + durations_min: Minimal duration in seconds for inclusion in slowest list. Default: 0.005. + verbose: Increase verbosity + no_header: Disable header + no_summary: Disable summary + quiet: Decrease verbosity + verbosity: Set verbosity. Default: 0. + show_extra_summary chars: Show extra test summary info as specified by chars: (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, (p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. (w)arnings are enabled by default (see --disable-warnings), 'N' can be used to reset the list. (default: 'fE'). + disable_pytest_warnings: Disable warnings summary + showlocals: Show locals in tracebacks (disabled by default) + no_showlocals: Hide locals in tracebacks (negate --showlocals passed through addopts) + traceback: Traceback print mode (auto/long/short/line/native/no) + show_capture: Controls how captured stdout/stderr/log is shown on failed tests. Default: all. + full_trace: Don't cut any tracebacks (default is to cut) + color: Color terminal output (yes/no/auto) + code_highlight {yes,no: Whether code should be highlighted (only if --color is also enabled). Default: yes. + pastebin: Send failed|all info to bpaste.net pastebin service + junit_xml: Create junit-xml style report file at given path + junit_prefix: Prepend prefix to classnames in junit-xml output + pythonwarnings PYTHONWARNINGS: Set which warnings to report, see -W option of Python itself + maxfail: Exit after first num failures or errors + strict_config: Any warnings encountered while parsing the `pytest` section of the configuration file raise errors + strict_markers: Markers not registered in the `markers` section of the configuration file raise errors + config_file file: Load configuration from `file` instead of trying to locate one of the implicit configuration files + continue_on_collection_errors: Force test execution even if collection errors occur + rootdir: Define root directory for tests. Can be relative path: 'root_dir', './root_dir', 'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: '$HOME/root_dir'. + collect_only: Only collect tests, don't execute them + pyargs: Try to interpret all arguments as Python packages + ignore: Ignore path during collection (multi-allowed) + ignore_glob: Ignore path pattern during collection (multi-allowed) + deselect: Deselect item (via node id prefix) during collection (multi-allowed) + confcutdir: Only load conftest.py's relative to specified dir + noconftest: Don't load any conftest.py files + keep_duplicates: Keep duplicate tests + collect_in_virtualenv: Don't ignore tests in a local virtualenv directory + import_mode: Prepend/append to sys.path when importing test modules and conftest files. Default: prepend. + doctest_modules: Run doctests in all .py modules + doctest_report: Choose another output format for diffs on doctest failure + doctest_glob pat: Doctests file matching pattern, default: test*.txt + doctest_ignore_import_errors: Ignore doctest ImportErrors + doctest_continue_on_failure: For a given doctest, continue to run after the first failure + basetemp dir: Base temporary directory for this test run. (Warning: this directory is removed if it exists.) + plugins name: Early-load given plugin module name or entry point (multi-allowed). To avoid loading of plugins, use the `no:` prefix, e.g. `no:doctest`. + no_plugins name: Early-load given plugin module name or entry point (multi-allowed). To avoid loading of plugins, use the `no:` prefix, e.g. `no:doctest`. + trace_config: Trace considerations of conftest.py files + debug : Store internal tracing debug information in this log file. This file is opened with 'w' and truncated as a result, care advised. Default: pytestdebug.log. + override_ini: Override ini option with "option value" style, e.g. `-o xfail_strict True -o cache_dir cache`. + assert_mode: Control assertion debugging tools. 'plain' performs no assertion debugging. 'rewrite' (the default) rewrites assert statements in test modules on import to provide assert expression information. + setup_only: Only setup fixtures, do not execute tests + setup_show: Show setup of fixtures while executing tests + setup_plan: Show what fixtures and tests would be executed but don't execute anything + log_level: Level of messages to catch/display. Not set by default, so it depends on the root/parent log handler's effective level, where it is "WARNING" by default. + log_format: Log format used by the logging module. + log_date_format: Log date format used by the logging module. + log_cli_level: logging level. + log_cli_format: Log format used by the logging module. + log_cli_date_format: Log date format used by the logging module. + log_file: Path to a file when logging will be written to. + log_file_level: Log file logging level. + log_file_format: Log format used by the logging module. + log_file_date_format: Log date format used by the logging module. + log_auto_indent: Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer. + """ + cli_args = list(paths) + + if select: + cli_args.append("-k") + cli_args.append(select) + + if select_markers: + cli_args.append("-m") + cli_args.append(select_markers) + + if markers: + cli_args.append("--markers") + + if exitfirst: + cli_args.append("--exitfirst") + + if fixtures: + cli_args.append("--fixtures") + + if fixtures_per_test: + cli_args.append("--fixtures-per-test") + + if pdb: + cli_args.append("--pdb") + + if pdbcls: + cli_args.append("--pdbcls") + cli_args.append(pdbcls) + + if trace: + cli_args.append("--trace") + + if capture: + cli_args.append("--capture") + + if runxfail: + cli_args.append("--runxfail") + + if last_failed: + cli_args.append("--last-failed") + + if failed_first: + cli_args.append("--failed-first") + + if new_first: + cli_args.append("--new-first") + + if cache_show: + cli_args.append("--cache-show") + cli_args.append(cache_show) + + if cache_clear: + cli_args.append("--cache-clear") + + if last_failed_no_failures: + cli_args.append("--last-failed-no-failures") + cli_args.append(last_failed_no_failures) + + if stepwise: + cli_args.append("--stepwise") + + if stepwise_skip: + cli_args.append("--stepwise-skip") + + if durations: + cli_args.append("--durations") + cli_args.append(str(durations)) + + if durations_min: + cli_args.append("--durations-min") + cli_args.append(str(durations_min)) + + if verbose: + cli_args.append("--verbose") + + if no_header: + cli_args.append("--no-header") + + if no_summary: + cli_args.append("--no-summary") + + if quiet: + cli_args.append("--quiet") + + if verbosity: + cli_args.append("--verbosity") + cli_args.append(str(verbosity)) + + if show_extra_summary: + cli_args.append("-r") + cli_args.append(show_extra_summary) + + if disable_pytest_warnings: + cli_args.append("--disable-pytest-warnings") + + if showlocals: + cli_args.append("--showlocals") + + if no_showlocals: + cli_args.append("--no-showlocals") + + if traceback: + cli_args.append("--tb") + cli_args.append(traceback) + + if show_capture: + cli_args.append("--show-capture") + cli_args.append(show_capture) + + if full_trace: + cli_args.append("--full-trace") + + if color: + cli_args.append("--color") + cli_args.append(color) + + if code_highlight: + cli_args.append("--code-highlight") + + if pastebin: + cli_args.append("--pastebin") + cli_args.append(pastebin) + + if junit_xml: + cli_args.append("--junit-xml") + cli_args.append(junit_xml) + + if junit_prefix: + cli_args.append("--junit-prefix") + cli_args.append(junit_prefix) + + if pythonwarnings: + cli_args.append("--pythonwarnings") + cli_args.append(pythonwarnings) + + if maxfail: + cli_args.append("--maxfail") + cli_args.append(str(maxfail)) + + if strict_config: + cli_args.append("--strict-config") + + if strict_markers: + cli_args.append("--strict-markers") + + if config_file: + cli_args.append("-c") + cli_args.append(config_file) + + if continue_on_collection_errors: + cli_args.append("--continue-on-collection-errors") + + if rootdir: + cli_args.append("--rootdir") + cli_args.append(rootdir) + + if collect_only: + cli_args.append("--collect-only") + + if pyargs: + cli_args.append("--pyargs") + + if ignore: + for ign in ignore: + cli_args.append("--ignore") + cli_args.append(ign) + + if ignore_glob: + for ign_glob in ignore_glob: + cli_args.append("--ignore-glob") + cli_args.append(ign_glob) + + if deselect: + cli_args.append("--deselect") + cli_args.append(deselect) + + if confcutdir: + cli_args.append("--confcutdir") + cli_args.append(confcutdir) + + if noconftest: + cli_args.append("--noconftest") + + if keep_duplicates: + cli_args.append("--keep-duplicates") + + if collect_in_virtualenv: + cli_args.append("--collect-in-virtualenv") + + if import_mode: + cli_args.append("--import-mode") + cli_args.append(import_mode) + + if doctest_modules: + cli_args.append("--doctest-modules") + + if doctest_report: + cli_args.append("--doctest-report") + cli_args.append(doctest_report) + + if doctest_glob: + cli_args.append("--doctest-glob") + cli_args.append(doctest_glob) + + if doctest_ignore_import_errors: + cli_args.append("--doctest-ignore-import-errors") + + if doctest_continue_on_failure: + cli_args.append("--doctest-continue-on-failure") + + if basetemp: + cli_args.append("--basetemp") + cli_args.append(basetemp) + + if plugins: + for plugin in plugins: + cli_args.append("-p") + cli_args.append(plugin) + + if no_plugins: + for no_plugin in no_plugins: + cli_args.append("-p") + cli_args.append(f"no:{no_plugin}") + + if trace_config: + cli_args.append("--trace-config") + + if debug: + cli_args.append("--debug") + cli_args.append(debug) + + if override_ini: + cli_args.append("--override-ini") + cli_args.append(override_ini) + + if assert_mode: + cli_args.append("--assert") + cli_args.append(assert_mode) + + if setup_only: + cli_args.append("--setup-only") + + if setup_show: + cli_args.append("--setup-show") + + if setup_plan: + cli_args.append("--setup-plan") + + if log_level: + cli_args.append("--log-level") + cli_args.append(log_level) + + if log_format: + cli_args.append("--log-format") + cli_args.append(log_format) + + if log_date_format: + cli_args.append("--log-date-format") + cli_args.append(log_date_format) + + if log_cli_level: + cli_args.append("--log-cli-level") + cli_args.extend(log_cli_level) + + if log_cli_format: + cli_args.append("--log-cli-format") + cli_args.append(log_cli_format) + + if log_cli_date_format: + cli_args.append("--log-cli-date-format") + cli_args.append(log_cli_date_format) + + if log_file: + cli_args.append("--log-file") + cli_args.append(log_file) + + if log_file_level: + cli_args.append("--log-file-level") + cli_args.append(log_file_level) + + if log_file_format: + cli_args.append("--log-file-format") + cli_args.append(log_file_format) + + if log_file_date_format: + cli_args.append("--log-file-date-format") + cli_args.append(log_file_date_format) + + if log_auto_indent: + cli_args.append("--log-auto-indent") + cli_args.append(log_auto_indent) + + return pytest(cli_args) diff --git a/src/duty/callables/ruff.py b/src/duty/callables/ruff.py new file mode 100644 index 0000000..ba4cccb --- /dev/null +++ b/src/duty/callables/ruff.py @@ -0,0 +1,302 @@ +"""Callable for [Ruff](https://github.com/charliermarsh/ruff).""" + +from __future__ import annotations + +import os +import subprocess +import sys +from functools import lru_cache + +from ruff.__main__ import find_ruff_bin + +from duty.callables import _named + + +@lru_cache(maxsize=None) +def _find_ruff() -> str: + try: + return find_ruff_bin() + except FileNotFoundError: + paths = os.environ["PATH"] + for path in paths.split(os.pathsep): + ruff = os.path.join(path, "ruff") + if os.path.exists(ruff): + return ruff + py_version = f"{sys.version_info[0]}.{sys.version_info[1]}" + pypackages_bin = os.path.join("__pypackages__", py_version, "bin") + ruff = os.path.join(pypackages_bin, "ruff") + if os.path.exists(ruff): + return ruff + return "ruff" + + +def _run(*args: str, verbose: bool = False, quiet: bool = False, silent: bool = False) -> int: + cli_args = list(args) + + if verbose: + cli_args.append("--verbose") + + if quiet: + cli_args.append("--quiet") + + if silent: + cli_args.append("--silent") + + process = subprocess.run([_find_ruff(), *cli_args], capture_output=True, text=True) + print(process.stdout) # noqa: T201 + return process.returncode + + +@_named("ruff.check") +def check( + *files: str, + config: str | None = None, + fix: bool | None = None, + show_source: bool | None = None, + show_fixes: bool | None = None, + diff: bool | None = None, + watch: bool | None = None, + fix_only: bool | None = None, + output_format: str | None = None, + statistics: bool | None = None, + add_noqa: bool | None = None, + show_files: bool | None = None, + show_settings: bool | None = None, + select: list[str] | None = None, + ignore: list[str] | None = None, + extend_select: list[str] | None = None, + per_file_ignores: dict[str, list[str]] | None = None, + fixable: list[str] | None = None, + unfixable: list[str] | None = None, + exclude: list[str] | None = None, + extend_exclude: list[str] | None = None, + respect_gitignore: bool | None = None, + force_exclude: bool | None = None, + no_cache: bool | None = None, + isolated: bool | None = None, + cache_dir: str | None = None, + stdin_filename: str | None = None, + exit_zero: bool | None = None, + exit_non_zero_on_fix: bool | None = None, + verbose: bool = False, + quiet: bool = False, + silent: bool = False, +) -> int: + """Run Ruff on the given files or directories. + + Parameters: + fix: Attempt to automatically fix lint violations + config: Path to the `pyproject.toml` or `ruff.toml` file to use for configuration + show_source: Show violations with source code + show_fixes: Show an enumeration of all autofixed lint violations + diff: Avoid writing any fixed files back; instead, output a diff for each changed file to stdout + watch: Run in watch mode by re-running whenever files change + fix_only: Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix` + output_format: Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, junit, grouped, github, gitlab, pylint] + statistics: Show counts for every rule with at least one violation + add_noqa: Enable automatic additions of `noqa` directives to failing lines + show_files: See the files Ruff will be run against with the current settings + show_settings: See the settings Ruff will use to lint a given Python file + select: Comma-separated list of rule codes to enable (or ALL, to enable all rules) + ignore: Comma-separated list of rule codes to disable + extend_select: Like --select, but adds additional rule codes on top of the selected ones + per_file_ignores: List of mappings from file pattern to code to exclude + fixable: List of rule codes to treat as eligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`) + unfixable: List of rule codes to treat as ineligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`) + exclude: List of paths, used to omit files and/or directories from analysis + extend_exclude: Like --exclude, but adds additional files and directories on top of those already excluded + respect_gitignore: Respect file exclusions via `.gitignore` and other standard ignore files + force_exclude: Enforce exclusions, even for paths passed to Ruff directly on the command-line + no_cache: Disable cache reads + isolated: Ignore all configuration files + cache_dir: Path to the cache directory [env: RUFF_CACHE_DIR=] + stdin_filename: The name of the file when passing it through stdin + exit_zero: Exit with status code "0", even upon detecting lint violations + exit_non_zero_on_fix: Exit with a non-zero status code if any files were modified via autofix, even if no lint violations remain + verbose: Enable verbose logging. + quiet: Print lint violations, but nothing else. + silent: Disable all logging (but still exit with status code "1" upon detecting lint violations). + """ + cli_args = list(files) + + if fix: + cli_args.append("--fix") + + if show_source: + cli_args.append("--show-source") + + if show_fixes: + cli_args.append("--show-fixes") + + if diff: + cli_args.append("--diff") + + if watch: + cli_args.append("--watch") + + if fix_only: + cli_args.append("--fix-only") + + if output_format: + cli_args.append("--format") + cli_args.append(output_format) + + if config: + cli_args.append("--config") + cli_args.append(config) + + if statistics: + cli_args.append("--statistics") + + if add_noqa: + cli_args.append("--add-noqa") + + if show_files: + cli_args.append("--show-files") + + if show_settings: + cli_args.append("--show-settings") + + if select: + cli_args.append("--select") + cli_args.append(",".join(select)) + + if ignore: + cli_args.append("--ignore") + cli_args.append(",".join(ignore)) + + if extend_select: + cli_args.append("--extend-select") + cli_args.append(",".join(extend_select)) + + if per_file_ignores: + cli_args.append("--per-file-ignores") + cli_args.append(" ".join(f"{path}:{','.join(codes)}" for path, codes in per_file_ignores.items())) + + if fixable: + cli_args.append("--fixable") + cli_args.append(",".join(fixable)) + + if unfixable: + cli_args.append("--unfixable") + cli_args.append(",".join(unfixable)) + + if exclude: + cli_args.append("--exclude") + cli_args.append(",".join(exclude)) + + if extend_exclude: + cli_args.append("--extend-exclude") + cli_args.append(",".join(extend_exclude)) + + if respect_gitignore: + cli_args.append("--respect-gitignore") + + if force_exclude: + cli_args.append("--force-exclude") + + if no_cache: + cli_args.append("--no-cache") + + if isolated: + cli_args.append("--isolated") + + if cache_dir: + cli_args.append("--cache-dir") + cli_args.append(cache_dir) + + if stdin_filename: + cli_args.append("--stdin-filename") + cli_args.append(stdin_filename) + + if exit_zero: + cli_args.append("--exit-zero") + + if exit_non_zero_on_fix: + cli_args.append("--exit-non-zero-on-fix") + + return _run("check", *cli_args, verbose=verbose, quiet=quiet, silent=silent) + + +@_named("ruff.rule") +def rule( + *, + output_format: str | None = None, + verbose: bool = False, + quiet: bool = False, + silent: bool = False, +) -> int: + """Explain a rule. + + Parameters: + output_format: Output format [default: pretty] [possible values: text, json, pretty]. + verbose: Enable verbose logging. + quiet: Print lint violations, but nothing else. + silent: Disable all logging (but still exit with status code "1" upon detecting lint violations). + """ + cli_args = [] + + if output_format: + cli_args.append("--format") + cli_args.append(output_format) + + return _run("rule", *cli_args, verbose=verbose, quiet=quiet, silent=silent) + + +@_named("ruff.config") +def config( + *, + verbose: bool = False, + quiet: bool = False, + silent: bool = False, +) -> int: + """List or describe the available configuration options. + + Parameters: + verbose: Enable verbose logging. + quiet: Print lint violations, but nothing else. + silent: Disable all logging (but still exit with status code "1" upon detecting lint violations). + """ + return _run("config", verbose=verbose, quiet=quiet, silent=silent) + + +@_named("ruff.linter") +def linter( + *, + output_format: str | None = None, + verbose: bool = False, + quiet: bool = False, + silent: bool = False, +) -> int: + """List all supported upstream linters. + + Parameters: + output_format: Output format [default: pretty] [possible values: text, json, pretty]. + verbose: Enable verbose logging. + quiet: Print lint violations, but nothing else. + silent: Disable all logging (but still exit with status code "1" upon detecting lint violations). + """ + cli_args = [] + + if output_format: + cli_args.append("--format") + cli_args.append(output_format) + + return _run("linter", *cli_args, verbose=verbose, quiet=quiet, silent=silent) + + +@_named("ruff.clean") +def clean( + *, + verbose: bool = False, + quiet: bool = False, + silent: bool = False, +) -> int: + """Clear any caches in the current directory and any subdirectories. + + Parameters: + verbose: Enable verbose logging. + quiet: Print lint violations, but nothing else. + silent: Disable all logging (but still exit with status code "1" upon detecting lint violations). + """ + return _run("clean", verbose=verbose, quiet=quiet, silent=silent) diff --git a/src/duty/callables/safety.py b/src/duty/callables/safety.py new file mode 100644 index 0000000..db0e149 --- /dev/null +++ b/src/duty/callables/safety.py @@ -0,0 +1,74 @@ +"""Callable for [Safety](https://github.com/pyupio/safety).""" + +from __future__ import annotations + +import importlib +import sys +from io import StringIO +from typing import Sequence, cast + +from duty.callables import _named + +# TODO: remove once support for Python 3.7 is dropped +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal + + +@_named("safety.check") +def check( + requirements: str | Sequence[str], + *, + ignore_vulns: dict[str, str] | None = None, + formatter: Literal["json", "bare", "text"] = "text", + full_report: bool = True, +) -> bool: + """Run the safety check command. + + This function makes sure we load the original, unpatched version of safety. + + Parameters: + requirements: Python "requirements" (list of pinned dependencies). + ignore_vulns: Vulnerabilities to ignore. + formatter: Report format. + full_report: Whether to output a full report. + + Returns: + Success/failure. + """ + # set default parameter values + ignore_vulns = ignore_vulns or {} + + # undo possible patching + # see https://github.com/pyupio/safety/issues/348 + for module in sys.modules: + if module.startswith("safety.") or module == "safety": + del sys.modules[module] + + importlib.invalidate_caches() + + # reload original, unpatched safety + from safety.formatter import SafetyFormatter + from safety.safety import calculate_remediations, check + from safety.util import read_requirements + + # check using safety as a library + if isinstance(requirements, (list, tuple, set)): + requirements = "\n".join(requirements) + packages = list(read_requirements(StringIO(cast(str, requirements)))) + vulns, db_full = check(packages=packages, ignore_vulns=ignore_vulns) + remediations = calculate_remediations(vulns, db_full) + output_report = SafetyFormatter(formatter).render_vulnerabilities( + announcements=[], + vulnerabilities=vulns, + remediations=remediations, + full=full_report, + packages=packages, + ) + + # print report, return status + if vulns: + print(output_report) # noqa: T201 + return False + return True