Skip to content
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
2 changes: 1 addition & 1 deletion src/maison/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
def _bootstrap_service(package_name: str) -> service.ConfigService:
_config_parser = config_parser.ConfigParser()

pyproject_parser = parsers.PyprojectParser(package_name=package_name)
pyproject_parser = parsers.PyprojectParser(tool_name=package_name)
toml_parser = parsers.TomlParser()
ini_parser = parsers.IniParser()

Expand Down
18 changes: 11 additions & 7 deletions src/maison/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
class Parser(typing.Protocol):
"""Defines the interface for a `Parser` class that's used to parse a config."""

def parse_config(self, file_path: pathlib.Path) -> typedefs.ConfigValues:
"""Parse a config file.
def parse_config(self, file: typing.BinaryIO) -> typedefs.ConfigValues:
"""Parse a config.

Args:
file_path: the path to the config file
file: the binary stream of the config file

Returns:
the config values
the parsed config
"""
...

Expand All @@ -42,18 +42,22 @@ def register_parser(
key = (suffix, stem)
self._parsers[key] = parser

def parse_config(self, file_path: pathlib.Path) -> typedefs.ConfigValues:
def parse_config(
self,
file_path: pathlib.Path,
file: typing.BinaryIO,
) -> typedefs.ConfigValues:
"""See `Parser.parse_config`."""
key: ParserDictKey

# First try (suffix, stem)
key = (file_path.suffix, file_path.stem)
if key in self._parsers:
return self._parsers[key].parse_config(file_path)
return self._parsers[key].parse_config(file)

# Then fallback to (suffix, None)
key = (file_path.suffix, None)
if key in self._parsers:
return self._parsers[key].parse_config(file_path)
return self._parsers[key].parse_config(file)

raise errors.UnsupportedConfigError(f"No parser registered for {file_path}")
4 changes: 4 additions & 0 deletions src/maison/disk_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,7 @@ def get_file_path(
return path / file_name

return None

def open_file(self, path: pathlib.Path) -> typing.BinaryIO:
"""See `Filesystem.open_file`."""
return path.open(mode="rb")
11 changes: 8 additions & 3 deletions src/maison/parsers/ini.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""A parser for .ini files."""

import configparser
import pathlib
import io
import typing

from maison import typedefs

Expand All @@ -12,8 +13,12 @@ class IniParser:
Implements the `Parser` protocol
"""

def parse_config(self, file_path: pathlib.Path) -> typedefs.ConfigValues:
def parse_config(self, file: typing.BinaryIO) -> typedefs.ConfigValues:
"""See the Parser.parse_config method."""
config = configparser.ConfigParser()
_ = config.read(file_path)
text_io = io.TextIOWrapper(file, encoding="utf-8")
try:
config.read_file(text_io)
except UnicodeDecodeError:
return {}
return {section: dict(config.items(section)) for section in config.sections()}
28 changes: 5 additions & 23 deletions src/maison/parsers/pyproject.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,19 @@
"""A parser for pyproject.toml files."""

import pathlib
import sys
from maison.parsers import toml


if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib

from maison import typedefs


class PyprojectParser:
class PyprojectParser(toml.TomlParser):
"""Responsible for parsing pyproject.toml files.

Implements the `Parser` protocol
"""

def __init__(self, package_name: str) -> None:
def __init__(self, tool_name: str) -> None:
"""Initialise the pyproject reader.

Args:
package_name: the name of the package to look for in file, e.g.
tool_name: the name of the package to look for in file, e.g.
`acme` part of `[tool.acme]`.
"""
self._package_name = package_name

def parse_config(self, file_path: pathlib.Path) -> typedefs.ConfigValues:
"""See the Parser.parse_config method."""
try:
with file_path.open(mode="rb") as fd:
pyproject_dict = dict(tomllib.load(fd))
except FileNotFoundError:
return {}
return dict(pyproject_dict.get("tool", {}).get(self._package_name, {}))
super().__init__(section_key=("tool", tool_name))
35 changes: 30 additions & 5 deletions src/maison/parsers/toml.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""A parser for .toml files."""

import pathlib
import sys


Expand All @@ -9,6 +8,8 @@
else:
import tomli as tomllib

import typing

from maison import typedefs


Expand All @@ -18,10 +19,34 @@ class TomlParser:
Implements the `Parser` protocol
"""

def parse_config(self, file_path: pathlib.Path) -> typedefs.ConfigValues:
def __init__(self, section_key: typing.Optional[tuple[str, ...]] = None) -> None:
"""Instantiate the class.

Args:
section_key: an optional toml section key/identifier to search for
within the toml. For example if the toml file contains:

[tool.my_section]
my_value = true

then setting `section_key=("tool", "my_section")` will return
`{"my_value": True}` as the config values.

"""
self.section_key = section_key or ()

def parse_config(self, file: typing.BinaryIO) -> typedefs.ConfigValues:
"""See the Parser.parse_config method."""
try:
with file_path.open(mode="rb") as fd:
return dict(tomllib.load(fd))
except (FileNotFoundError, tomllib.TOMLDecodeError):
values = dict(tomllib.load(file))
except tomllib.TOMLDecodeError:
return {}

current = values
for key in self.section_key:
if key in current and isinstance(current[key], dict):
current = current[key]
else:
return {}

return current
19 changes: 18 additions & 1 deletion src/maison/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,33 @@ def get_file_path(
Returns:
The `Path` to the file if it exists or `None` if it doesn't
"""
...

def open_file(self, path: pathlib.Path) -> typing.BinaryIO:
"""Open a file.

Args:
path: the path to the file

Returns:
the opened file as a binary I/O stream
"""
...


class ConfigParser(typing.Protocol):
"""Defines the interface for a class that parses a config."""

def parse_config(self, file_path: pathlib.Path) -> typedefs.ConfigValues:
def parse_config(
self,
file_path: pathlib.Path,
file: typing.BinaryIO,
) -> typedefs.ConfigValues:
"""Parse a config.

Args:
file_path: the path to a config file.
file: the binary I/O stream of the file.

Returns:
the parsed config
Expand Down
3 changes: 2 additions & 1 deletion src/maison/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ def get_config_values(
config_values: typedefs.ConfigValues = {}

for path in config_file_paths:
parsed_config = self.config_parser.parse_config(path)
file = self.filesystem.open_file(path=path)
parsed_config = self.config_parser.parse_config(file_path=path, file=file)
config_values = utils.deep_merge(config_values, parsed_config)

if not merge_configs:
Expand Down
90 changes: 0 additions & 90 deletions tests/integration_tests/parsers/test_ini.py

This file was deleted.

Loading