Skip to content

Commit

Permalink
Add pyproject.toml support (#99) (#100)
Browse files Browse the repository at this point in the history
* Add pyproject.toml support (#99)

Co-authored-by: kasium <15907922+kasium@users.noreply.github.com>
  • Loading branch information
seddonym and kasium committed Jul 29, 2021
1 parent 1250c7d commit 27e6bc6
Show file tree
Hide file tree
Showing 17 changed files with 327 additions and 43 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,7 @@ docs/_build
.python-version

pip-wheel-metadata

# vscode
.vscode/
.history/
29 changes: 17 additions & 12 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
language: python
sudo: false
cache: pip
env:
global:
- LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so
- SEGFAULT_SIGNALS=all
matrix:
jobs:
include:
- python: '3.6'
env:
- TOXENV=py36,report,check,docs
- TOXENV=py36-notoml,report,check,docs
- python: '3.7'
dist: xenial
sudo: required
env:
- TOXENV=py37,report
- TOXENV=py37-notoml,report
- python: '3.8'
dist: xenial
sudo: required
env:
- TOXENV=py38,report
- TOXENV=py38-notoml,report
- python: '3.9'
dist: xenial
sudo: required
env:
- TOXENV=py39,report
- TOXENV=py39-notoml,report
- python: '3.6'
env:
- TOXENV=py36-toml,report
- python: '3.7'
env:
- TOXENV=py37-toml,report
- python: '3.8'
env:
- TOXENV=py38-toml,report
- python: '3.9'
env:
- TOXENV=py39-toml,report
before_install:
- python --version
- uname -a
Expand Down
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ Contributors
* Daniel Jurczak - https://github.com/danieljurczak
* Ben Warren - https://github.com/bwarren
* Aaron Gokaslan - https://github.com/Skylion007
* Kai Mueller - https://github.com/kasium
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changelog
=========

latest
------

* Add support for TOML configuration files.

1.2.2 (2021-07-13)
------------------

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Contents
usage
contract_types
custom_contract_types
toml
contributing
authors
changelog
Expand Down
46 changes: 46 additions & 0 deletions docs/toml.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
TOML support
------------

While all the examples are in INI format, Import Linter also supports TOML.

The TOML configuration is very similar to the others with a few differences:

- the sections must start with ``tool.``
- contracts are defined by ``[[tool.importlinter.contracts]]``

The basic configuration layout looks like:

.. code-block:: toml
[tool.importlinter]
root_package = mypackage
[[tool.importlinter.contracts]]
name = Contract One
[[tool.importlinter.contracts]]
name = Contract Two
Following, an example with a layered configuration:

.. code-block:: toml
[tool.importlinter]
root_packages = [
"high",
"medium",
"low",
]
[[tool.importlinter.contracts]]
name = "My three-tier layers contract (multiple root packages)"
type = "layers"
layers = [
"high",
"medium",
"low",
]
Please note, that in order to use TOML files, you need to install the extra require ``toml``::

pip install import-linter[toml]
15 changes: 7 additions & 8 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ Usage
Configuration file location
---------------------------

Before running the linter, you need to supply configuration in a file, in INI format. Import Linter will look in the
current directory for one of the following files:
Before running the linter, you need to supply configuration in a file.
If not specified over the command line, Import Linter will look in the current directory for one of the following files:
- ``setup.cfg`` (INI format)
- ``.importlinter`` (INI format)
- ``pyproject.toml`` (TOML format)

- ``setup.cfg``
- ``.importlinter``

(Different filenames / locations can be specified as a command line argument, see below.)

Top level configuration
-----------------------
Expand Down Expand Up @@ -90,8 +89,8 @@ Running this will check that your project adheres to the contracts you've define
**Arguments:**

- ``--config``:
The configuration file to use. If not supplied, Import Linter will look for ``setup.cfg``
or ``.importlinter`` in the current directory. (Optional.)
(optional) The configuration file to use. This overrides the default file search strategy.
By default it's assumed that the file is an ini-file unless the file extension is ``toml``.

**Default usage:**

Expand Down
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
markers =
toml_not_installed: marks tests as only to be run if toml is not installed
toml_installed: marks tests as only to be run if toml is installed
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def read(*names, **kwargs):
],
python_requires=">=3.6",
install_requires=["click>=6,<9", "grimp>=1.2.3,<2"],
extra_requires={"toml": ["toml"]},
entry_points={
"console_scripts": ["lint-imports = importlinter.cli:lint_imports_command"]
},
Expand Down
72 changes: 60 additions & 12 deletions src/importlinter/adapters/user_options.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import configparser
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, List
import abc

try:
import toml

_HAS_TOML = True
except ImportError:
_HAS_TOML = False

from importlinter.application import file_finding
from importlinter.application.app_config import settings
from importlinter.application.ports import user_options as ports
from importlinter.application.user_options import UserOptions


class IniFileUserOptionReader(ports.UserOptionReader):
"""
Reader that looks for and parses the contents of INI files.
"""
class AbstractUserOptionReader(ports.UserOptionReader):

potential_config_filenames = ("setup.cfg", ".importlinter")
section_name = "importlinter"
potential_config_filenames: List[str]

def read_options(self, config_filename: Optional[str] = None) -> Optional[UserOptions]:
if config_filename:
Expand All @@ -27,11 +31,31 @@ def read_options(self, config_filename: Optional[str] = None) -> Optional[UserOp
return None

for config_filename in config_filenames:
config = configparser.ConfigParser()
file_contents = settings.FILE_SYSTEM.read(config_filename)
config.read_string(file_contents)
if self.section_name in config.sections():
return self._build_from_config(config)
options = self._read_config_filename(config_filename)
if options:
return options

return None

@abc.abstractmethod
def _read_config_filename(self, config_filename: str) -> Optional[UserOptions]:
raise NotImplementedError


class IniFileUserOptionReader(AbstractUserOptionReader):
"""
Reader that looks for and parses the contents of INI files.
"""

potential_config_filenames = ["setup.cfg", ".importlinter"]
section_name = "importlinter"

def _read_config_filename(self, config_filename: str) -> Optional[UserOptions]:
config = configparser.ConfigParser()
file_contents = settings.FILE_SYSTEM.read(config_filename)
config.read_string(file_contents)
if self.section_name in config.sections():
return self._build_from_config(config)
return None

def _build_from_config(self, config: configparser.ConfigParser) -> UserOptions:
Expand All @@ -51,3 +75,27 @@ def _clean_section_config(section_config: Dict[str, Any]) -> Dict[str, Any]:
else:
section_dict[key] = value.strip().split("\n")
return section_dict


class TomlFileUserOptionReader(AbstractUserOptionReader):
"""
Reader that looks for and parses the contents of TOML files.
"""

section_name = "importlinter"
potential_config_filenames = ["pyproject.toml"]

def _read_config_filename(self, config_filename: str) -> Optional[UserOptions]:
if not _HAS_TOML:
return None

file_contents = settings.FILE_SYSTEM.read(config_filename)
data = toml.loads(file_contents)

tool_data = data.get("tool", {})
session_options = tool_data.get("importlinter", {})
if not session_options:
return None

contracts = session_options.pop("contracts", [])
return UserOptions(session_options=session_options, contracts_options=contracts)
9 changes: 8 additions & 1 deletion src/importlinter/application/use_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,14 @@ def create_report(user_options: UserOptions) -> Report:


def _read_user_options(config_filename: Optional[str] = None) -> UserOptions:
for reader in settings.USER_OPTION_READERS:
readers = settings.USER_OPTION_READERS.values()
if config_filename:
if config_filename.endswith(".toml"):
readers = [settings.USER_OPTION_READERS["toml"]]
else:
readers = [settings.USER_OPTION_READERS["ini"]]

for reader in readers:
options = reader.read_options(config_filename=config_filename)
if options:
normalized_options = _normalize_user_options(options)
Expand Down
7 changes: 5 additions & 2 deletions src/importlinter/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
from .adapters.building import GraphBuilder
from .adapters.filesystem import FileSystem
from .adapters.printing import ClickPrinter
from .adapters.user_options import IniFileUserOptionReader
from .adapters.user_options import IniFileUserOptionReader, TomlFileUserOptionReader
from .application import use_cases
from .application.app_config import settings

settings.configure(
USER_OPTION_READERS=[IniFileUserOptionReader()],
USER_OPTION_READERS={
"ini": IniFileUserOptionReader(),
"toml": TomlFileUserOptionReader(),
},
GRAPH_BUILDER=GraphBuilder(),
PRINTER=ClickPrinter(),
FILE_SYSTEM=FileSystem(),
Expand Down
12 changes: 12 additions & 0 deletions tests/assets/testpackage/.customkeptcontract.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[tool.importlinter]
root_package = "testpackage"
contract_types = [
"forbidden_import: tests.helpers.contracts.ForbiddenImportContract"
]


[[tool.importlinter.contracts]]
name = "Custom kept contract"
type = "forbidden_import"
importer = "testpackage.utils"
imported = "testpackage.low"
13 changes: 12 additions & 1 deletion tests/functional/test_lint_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,25 @@
(testpackage_directory, ".brokencontract.ini", cli.EXIT_STATUS_ERROR),
(testpackage_directory, ".malformedcontract.ini", cli.EXIT_STATUS_ERROR),
(testpackage_directory, ".customkeptcontract.ini", cli.EXIT_STATUS_SUCCESS),
pytest.param(
testpackage_directory,
".customkeptcontract.toml",
cli.EXIT_STATUS_ERROR,
marks=pytest.mark.toml_not_installed,
),
pytest.param(
testpackage_directory,
".customkeptcontract.toml",
cli.EXIT_STATUS_SUCCESS,
marks=pytest.mark.toml_installed,
),
(testpackage_directory, ".externalkeptcontract.ini", cli.EXIT_STATUS_SUCCESS),
(testpackage_directory, ".externalbrokencontract.ini", cli.EXIT_STATUS_ERROR),
(multipleroots_directory, ".multiplerootskeptcontract.ini", cli.EXIT_STATUS_SUCCESS),
(multipleroots_directory, ".multiplerootsbrokencontract.ini", cli.EXIT_STATUS_ERROR),
),
)
def test_lint_imports(working_directory, config_filename, expected_result):

os.chdir(working_directory)

if config_filename:
Expand Down

0 comments on commit 27e6bc6

Please sign in to comment.