Skip to content

Commit

Permalink
Merge pull request #200 from mbalatsko/support-pyproject-toml-config
Browse files Browse the repository at this point in the history
Support parsing input parametres from `pyproject.toml` config
  • Loading branch information
raimon49 authored Jul 11, 2024
2 parents b3a3d87 + ee26a1c commit ef8805a
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 25 deletions.
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)

0 comments on commit ef8805a

Please sign in to comment.