Skip to content

Commit

Permalink
Merge pull request #222 from villainy/toml
Browse files Browse the repository at this point in the history
Add optional TOML configuration support
  • Loading branch information
klen authored Aug 8, 2022
2 parents 75420f4 + 08fca02 commit 6f9ee40
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 17 deletions.
80 changes: 74 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ Installation:

$ pip install pylama

TOML configuration can be enabled optionally: ::

$ pip install pylama[toml]

You may optionally install the requirements with the library: ::

$ pip install pylama[mypy]
Expand Down Expand Up @@ -199,25 +203,29 @@ Just add ``# noqa`` at the end of a line to ignore:
.. _config:

Configuration file
------------------
==================

**Pylama** looks for a configuration file in the current directory.

You can use a “global” configuration, stored in `.pylama.ini` in your home
directory. This will be used as a fallback configuration.

The program searches for the first matching ini-style configuration file in
the directories of command line argument. Pylama looks for the configuration
in this order: ::
The program searches for the first matching configuration file in the
directories of command line argument. Pylama looks for the configuration in
this order: ::

./pylama.ini
./pyproject.toml
./setup.cfg
./tox.ini
./pytest.ini
~/.pylama.ini

The ``--option`` / ``-o`` argument can be used to specify a configuration file.

INI-style configuration
-----------------------

Pylama searches for sections whose names start with `pylama`.

The `pylama` section configures global options like `linters` and `skip`.
Expand All @@ -231,7 +239,7 @@ The `pylama` section configures global options like `linters` and `skip`.
ignore = F0401,C0111,E731

Set code-checkers' options
--------------------------
^^^^^^^^^^^^^^^^^^^^^^^^^^

You can set options for a special code checkers with pylama configurations.

Expand All @@ -253,7 +261,7 @@ replaced by underscores (e.g. Pylint's ``max-line-length`` becomes


Set options for file (group of files)
-------------------------------------
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

You can set options for special file (group of files)
with sections:
Expand All @@ -272,6 +280,66 @@ The options have a higher priority than in the `pylama` section.
[pylama:*/setup.py]
skip = 1

TOML configuration
-----------------------

Pylama searches for sections whose names start with `tool.pylama`.

The `tool.pylama` section configures global options like `linters` and `skip`.

::

[tool.pylama]
format = "pylint"
skip = "*/.tox/*,*/.env/*"
linters = "pylint,mccabe"
ignore = "F0401,C0111,E731"

Set code-checkers' options
^^^^^^^^^^^^^^^^^^^^^^^^^^

You can set options for a special code checkers with pylama configurations.

::

[tool.pylama.linter.pyflakes]
builtins = "_"

[tool.pylama.linter.pycodestyle]
max_line_length = 100

[tool.pylama.linter.pylint]
max_line_length = 100
disable = "R"

See code-checkers' documentation for more info. Note that dashes are
replaced by underscores (e.g. Pylint's ``max-line-length`` becomes
``max_line_length``).


Set options for file (group of files)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

You can set options for special file (group of files)
with sections:

The options have a higher priority than in the `tool.pylama` section.

::

[[tool.pylama.files]]
path = "*/pylama/main.py"
ignore = "C901,R0914,W0212"
select = "R"

[[tool.pylama.files]]
path = "pylama:*/tests.py"
ignore = "C0110"

[[tool.pylama.files]]
path = "pylama:*/setup.py"
skip = 1


Pytest integration
==================
Expand Down
41 changes: 33 additions & 8 deletions pylama/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@
from pylama.libs import inirama
from pylama.lint import LINTERS

try:
from pylama import config_toml
CONFIG_FILES = ["pylama.ini", "pyproject.toml", "setup.cfg", "tox.ini", "pytest.ini"]
except ImportError:
CONFIG_FILES = ["pylama.ini", "setup.cfg", "tox.ini", "pytest.ini"]


#: A default checkers
DEFAULT_LINTERS = "pycodestyle", "pyflakes", "mccabe"

CURDIR = Path.cwd()
HOMECFG = Path.home() / ".pylama.ini"
CONFIG_FILES = "pylama.ini", "setup.cfg", "tox.ini", "pytest.ini"
DEFAULT_SECTION = "pylama"

# Setup a logger
LOGGER.propagate = False
Expand Down Expand Up @@ -253,18 +260,36 @@ def process_value(actions: Dict, name: str, value: Any) -> Any:
return value


def get_config(ini_path: str = None, rootdir: Path = None) -> inirama.Namespace:
"""Load configuration from INI."""
config = inirama.Namespace()
config.default_section = "pylama"

cfg_path = ini_path or get_default_config_file(rootdir)
def get_config(user_path: str = None, rootdir: Path = None) -> inirama.Namespace:
"""Load configuration from files."""
cfg_path = user_path or get_default_config_file(rootdir)
if not cfg_path and HOMECFG.exists():
cfg_path = HOMECFG.as_posix()

if cfg_path:
LOGGER.info("Read config: %s", cfg_path)
config.read(cfg_path)
if cfg_path.endswith(".toml"):
return get_config_toml(cfg_path)
else:
return get_config_ini(cfg_path)

return inirama.Namespace()


def get_config_ini(ini_path: str) -> inirama.Namespace:
"""Load configuration from INI."""
config = inirama.Namespace()
config.default_section = DEFAULT_SECTION
config.read(ini_path)

return config


def get_config_toml(toml_path: str) -> inirama.Namespace:
"""Load configuration from TOML."""
config = config_toml.Namespace()
config.default_section = DEFAULT_SECTION
config.read(toml_path)

return config

Expand Down
31 changes: 31 additions & 0 deletions pylama/config_toml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Pylama TOML configuration."""

import toml

from pylama.libs.inirama import Namespace as _Namespace


class Namespace(_Namespace):
"""Inirama-style wrapper for TOML config."""

def parse(self, source: str, update: bool = True, **params):
"""Parse TOML source as string."""
content = toml.loads(source)
tool = content.get("tool", {})
pylama = tool.get("pylama", {})
linters = pylama.pop("linter", {})
files = pylama.pop("files", [])

for name, value in pylama.items():
self["pylama"][name] = value

for linter, options in linters.items():
for name, value in options.items():
self[f"pylama:{linter}"][name] = value

for file in files:
path = file.pop("path", None)
if path is None:
continue
for name, value in file.items():
self[f"pylama:{path}"][name] = value
2 changes: 1 addition & 1 deletion pylama/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def run(
sorter = default_sorter
if options and options.sort:
sort = options.sort
sorter = lambda err: (sort.get(err.etype, 999), err.lnum)
sorter = lambda err: (sort.get(err.etype, 999), err.lnum) # pylint: disable=C3001

return sorted(errors, key=sorter)

Expand Down
2 changes: 2 additions & 0 deletions requirements/requirements-tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ radon >= 5.1.0
mypy
pylint >= 2.11.1
pylama-quotes
toml
vulture

types-setuptools
types-toml
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ max_line_length = 100
builtins = _

[pylama:pylint]
ignore=R,E1002,W0511,C0103,C0204
ignore=R,E1002,W0511,C0103,C0204,W0012

[pylama:pylama/core.py]
ignore = C901,R0914
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def parse_requirements(path: str) -> "list[str]":
install_requires=parse_requirements("requirements/requirements.txt"),
extras_require=dict(
tests=parse_requirements("requirements/requirements-tests.txt"),
all=OPTIONAL_LINTERS, **{linter: [linter] for linter in OPTIONAL_LINTERS}
all=OPTIONAL_LINTERS, **{linter: [linter] for linter in OPTIONAL_LINTERS},
toml="toml>=0.10.2",
),
)
74 changes: 74 additions & 0 deletions tests/test_config_toml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Test TOML config handling."""

from unittest import mock

from pylama import config_toml
from pylama.config import DEFAULT_SECTION
from pylama.libs import inirama

CONFIG_TOML = """
[tool.pylama]
async = 1
ignore = "D203,D213,F0401,C0111,E731,I0011"
linters = "pycodestyle,pyflakes,mccabe,pydocstyle,pylint,mypy"
skip = "pylama/inirama.py,pylama/libs/*"
verbose = 0
max_line_length = 100
[tool.pylama.linter.pyflakes]
builtins = "_"
[tool.pylama.linter.pylint]
ignore = "R,E1002,W0511,C0103,C0204"
[[tool.pylama.files]]
path = "pylama/core.py"
ignore = "C901,R0914"
[[tool.pylama.files]]
path = "pylama/main.py"
ignore = "R0914,W0212,C901,E1103"
[[tool.pylama.files]]
path = "tests/*"
ignore = "D,C,W,E1103"
"""

CONFIG_INI="""
[pylama]
async = 1
ignore = D203,D213,F0401,C0111,E731,I0011
linters = pycodestyle,pyflakes,mccabe,pydocstyle,pylint,mypy
skip = pylama/inirama.py,pylama/libs/*
verbose = 0
max_line_length = 100
[pylama:pyflakes]
builtins = _
[pylama:pylint]
ignore=R,E1002,W0511,C0103,C0204
[pylama:pylama/core.py]
ignore = C901,R0914
[pylama:pylama/main.py]
ignore = R0914,W0212,C901,E1103
[pylama:tests/*]
ignore = D,C,W,E1103
"""

def test_toml_parsing_matches_ini():
"""Ensure the parsed TOML namepsace matches INI parsing."""
with mock.patch("pylama.libs.inirama.io.open", mock.mock_open(read_data=CONFIG_INI)):
ini = inirama.Namespace()
ini.default_section = DEFAULT_SECTION
ini.read("ini")

with mock.patch("pylama.libs.inirama.io.open", mock.mock_open(read_data=CONFIG_TOML)):
toml = config_toml.Namespace()
toml.default_section = DEFAULT_SECTION
toml.read("toml")

assert ini.sections == toml.sections

0 comments on commit 6f9ee40

Please sign in to comment.