Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support parsing input parametres from pyproject.toml config #200

Merged
merged 4 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Dump the software license list of Python packages installed with pip.
* [Option: fail\-on](#option-fail-on)
* [Option: allow\-only](#option-allow-only)
* [Option: partial\-match](#option-partial-match)
* [pyproject.toml support](#pyproject-toml-support)
* [More Information](#more-information)
* [Dockerfile](#dockerfile)
* [About UnicodeEncodeError](#about-unicodeencodeerror)
Expand Down Expand Up @@ -589,6 +590,25 @@ $ echo $?
0
```

### pyproject.toml support

All command-line options for `pip-licenses` can be configured using the `pyproject.toml` file under the `[tool.pip-licenses]` section.
The `pyproject.toml` file is searched in the directory where the `pip-licenses` script is executed.
Command-line options specified during execution will override the corresponding options in `pyproject.toml`.

Example `pyproject.toml` configuration:

```toml
[tool.pip-licences]
from = "classifier"
ignore-packages = [
"scipy"
]
fail-on = "MIT;"
```

If you run `pip-licenses` without any command-line options, all options will be taken from the `pyproject.toml` file.
For instance, if you run `pip-licenses --from=mixed`, the `from` option will be overridden to `mixed`, while all other options will be sourced from `pyproject.toml`.

### More Information

Expand Down Expand Up @@ -664,6 +684,8 @@ See useful reports:
* [prettytable](https://pypi.org/project/prettytable/) by Luke Maurits and maintainer of fork version Jazzband team under the BSD-3-Clause License
* **Note:** This package implicitly requires [wcwidth](https://pypi.org/project/wcwidth/).

* [tomli](https://pypi.org/project/tomli/) by Taneli Hukkinen under the MIT License

`pip-licenses` has been implemented in the policy to minimize the dependence on external package.

## Uninstallation
Expand Down
1 change: 1 addition & 0 deletions dev-requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ pytest-cov
pytest-pycodestyle
pytest-runner
twine
tomli-w
2 changes: 2 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ tomli==2.0.1
# mypy
# pep517
# pytest
tomli-w==1.0.0
# via -r dev-requirements.in
twine==4.0.2
# via -r dev-requirements.in
typing-extensions==4.10.0
Expand Down
72 changes: 48 additions & 24 deletions piplicenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from pathlib import Path
from typing import TYPE_CHECKING, Iterable, List, Type, cast

import tomli
from prettytable import ALL as RULE_ALL
from prettytable import FRAME as RULE_FRAME
from prettytable import HEADER as RULE_HEADER
Expand Down Expand Up @@ -878,6 +879,12 @@ def choices_from_enum(enum_cls: Type[NoValueEnum]) -> List[str]:
]


def get_value_from_enum(
enum_cls: Type[NoValueEnum], value: str
) -> NoValueEnum:
return getattr(enum_cls, value_to_enum_key(value))


MAP_DEST_TO_ENUM = {
"from_": FromArg,
"order": OrderArg,
Expand All @@ -894,15 +901,25 @@ def __call__( # type: ignore[override]
option_string: Optional[str] = None,
) -> None:
enum_cls = MAP_DEST_TO_ENUM[self.dest]
values = value_to_enum_key(values)
setattr(namespace, self.dest, getattr(enum_cls, values))
setattr(namespace, self.dest, get_value_from_enum(enum_cls, values))


def load_config_from_file(pyproject_path: str):
if Path(pyproject_path).exists():
with open(pyproject_path, "rb") as f:
return tomli.load(f).get("tool", {}).get(__pkgname__, {})
return {}

def create_parser() -> CompatibleArgumentParser:

def create_parser(
pyproject_path: str = "pyproject.toml",
) -> CompatibleArgumentParser:
parser = CompatibleArgumentParser(
description=__summary__, formatter_class=CustomHelpFormatter
)

config_from_file = load_config_from_file(pyproject_path)

common_options = parser.add_argument_group("Common options")
format_options = parser.add_argument_group("Format options")
verify_options = parser.add_argument_group("Verify options")
Expand All @@ -914,7 +931,7 @@ def create_parser() -> CompatibleArgumentParser:
common_options.add_argument(
"--python",
type=str,
default=sys.executable,
default=config_from_file.get("python", sys.executable),
metavar="PYTHON_EXEC",
help="R| path to python executable to search distributions from\n"
"Package will be searched in the selected python's sys.path\n"
Expand All @@ -927,7 +944,9 @@ def create_parser() -> CompatibleArgumentParser:
dest="from_",
action=SelectAction,
type=str,
default=FromArg.MIXED,
default=get_value_from_enum(
FromArg, config_from_file.get("from", "mixed")
),
metavar="SOURCE",
choices=choices_from_enum(FromArg),
help="R|where to find license information\n"
Expand All @@ -939,7 +958,9 @@ def create_parser() -> CompatibleArgumentParser:
"--order",
action=SelectAction,
type=str,
default=OrderArg.NAME,
default=get_value_from_enum(
OrderArg, config_from_file.get("order", "name")
),
metavar="COL",
choices=choices_from_enum(OrderArg),
help="R|order by column\n"
Expand All @@ -952,7 +973,9 @@ def create_parser() -> CompatibleArgumentParser:
dest="format_",
action=SelectAction,
type=str,
default=FormatArg.PLAIN,
default=get_value_from_enum(
FormatArg, config_from_file.get("format", "plain")
),
metavar="STYLE",
choices=choices_from_enum(FormatArg),
help="R|dump as set format style\n"
Expand All @@ -964,12 +987,13 @@ def create_parser() -> CompatibleArgumentParser:
common_options.add_argument(
"--summary",
action="store_true",
default=False,
default=config_from_file.get("summary", False),
help="dump summary of each license",
)
common_options.add_argument(
"--output-file",
action="store",
default=config_from_file.get("output-file"),
type=str,
help="save license list to file",
)
Expand All @@ -980,7 +1004,7 @@ def create_parser() -> CompatibleArgumentParser:
type=str,
nargs="+",
metavar="PKG",
default=[],
default=config_from_file.get("ignore-packages", []),
help="ignore package name in dumped list",
)
common_options.add_argument(
Expand All @@ -990,83 +1014,83 @@ def create_parser() -> CompatibleArgumentParser:
type=str,
nargs="+",
metavar="PKG",
default=[],
default=config_from_file.get("packages", []),
help="only include selected packages in output",
)
format_options.add_argument(
"-s",
"--with-system",
action="store_true",
default=False,
default=config_from_file.get("with-system", False),
help="dump with system packages",
)
format_options.add_argument(
"-a",
"--with-authors",
action="store_true",
default=False,
default=config_from_file.get("with-authors", False),
help="dump with package authors",
)
format_options.add_argument(
"--with-maintainers",
action="store_true",
default=False,
default=config_from_file.get("with-maintainers", False),
help="dump with package maintainers",
)
format_options.add_argument(
"-u",
"--with-urls",
action="store_true",
default=False,
default=config_from_file.get("with-urls", False),
help="dump with package urls",
)
format_options.add_argument(
"-d",
"--with-description",
action="store_true",
default=False,
default=config_from_file.get("with-description", False),
help="dump with short package description",
)
format_options.add_argument(
"-nv",
"--no-version",
action="store_true",
default=False,
default=config_from_file.get("no-version", False),
help="dump without package version",
)
format_options.add_argument(
"-l",
"--with-license-file",
action="store_true",
default=False,
default=config_from_file.get("with-license-file", False),
help="dump with location of license file and "
"contents, most useful with JSON output",
)
format_options.add_argument(
"--no-license-path",
action="store_true",
default=False,
default=config_from_file.get("no-license-path", False),
help="I|when specified together with option -l, "
"suppress location of license file output",
)
format_options.add_argument(
"--with-notice-file",
action="store_true",
default=False,
default=config_from_file.get("with-notice-file", False),
help="I|when specified together with option -l, "
"dump with location of license file and contents",
)
format_options.add_argument(
"--filter-strings",
action="store_true",
default=False,
default=config_from_file.get("filter-strings", False),
help="filter input according to code page",
)
format_options.add_argument(
"--filter-code-page",
action="store",
type=str,
default="latin1",
default=config_from_file.get("filter-code-page", "latin1"),
metavar="CODE",
help="I|specify code page for filtering " "(default: %(default)s)",
)
Expand All @@ -1075,22 +1099,22 @@ def create_parser() -> CompatibleArgumentParser:
"--fail-on",
action="store",
type=str,
default=None,
default=config_from_file.get("fail-on", None),
help="fail (exit with code 1) on the first occurrence "
"of the licenses of the semicolon-separated list",
)
verify_options.add_argument(
"--allow-only",
action="store",
type=str,
default=None,
default=config_from_file.get("allow-only", None),
help="fail (exit with code 1) on the first occurrence "
"of the licenses not in the semicolon-separated list",
)
verify_options.add_argument(
"--partial-match",
action="store_true",
default=False,
default=config_from_file.get("partial-match", False),
help="enables partial matching for --allow-only/--fail-on",
)

Expand Down
1 change: 1 addition & 0 deletions requirements.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
prettytable
tomli
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
#
prettytable==3.9.0
# via -r requirements.in
tomli==2.0.1
# via -r requirements.in
wcwidth==0.2.13
# via prettytable
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ setup_requires =
pytest-runner
install_requires =
prettytable >= 2.3.0
tomli >= 2
tests_require =
docutils
mypy
Expand All @@ -43,6 +44,7 @@ test =
pytest-cov
pytest-pycodestyle
pytest-runner
tomli-w

[bdist_wheel]
universal = 0
Expand Down
54 changes: 53 additions & 1 deletion test_piplicenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

import copy
import email
import json
import os
import re
import sys
import tempfile
import unittest
import venv
from enum import Enum, auto
Expand All @@ -19,6 +20,7 @@
import docutils.parsers.rst
import docutils.utils
import pytest
import tomli_w
from _pytest.capture import CaptureFixture

import piplicenses
Expand Down Expand Up @@ -1108,3 +1110,53 @@ def test_extract_homepage_project_uprl_fallback_capitalisation() -> None:
assert "homepage" == extract_homepage(metadata=metadata) # type: ignore

metadata.get_all.assert_called_once_with("Project-URL", [])


def test_pyproject_toml_args_parsed_correctly():
# we test that parameters of different types are deserialized correctly
pyptoject_conf = {
"tool": {
__pkgname__: {
# choices_from_enum
"from": "classifier",
# bool
"summary": True,
# list[str]
"ignore-packages": ["package1", "package2"],
# str
"fail-on": "LIC1;LIC2",
}
}
}

toml_str = tomli_w.dumps(pyptoject_conf)

# Create a temporary file and write the TOML string to it
with tempfile.NamedTemporaryFile(
suffix=".toml", delete=False
) as temp_file:
temp_file.write(toml_str.encode("utf-8"))

parser = create_parser(temp_file.name)
args = parser.parse_args([])

tool_conf = pyptoject_conf["tool"][__pkgname__]

# assert values are correctly parsed from toml
assert args.from_ == FromArg.CLASSIFIER
assert args.summary == tool_conf["summary"]
assert args.ignore_packages == tool_conf["ignore-packages"]
assert args.fail_on == tool_conf["fail-on"]

# assert args are rewritable using cli
args = parser.parse_args(["--from=meta"])

assert args.from_ != FromArg.CLASSIFIER
assert args.from_ == FromArg.META

# all other are parsed from toml
assert args.summary == tool_conf["summary"]
assert args.ignore_packages == tool_conf["ignore-packages"]
assert args.fail_on == tool_conf["fail-on"]

os.unlink(temp_file.name)