Skip to content

Commit

Permalink
Merge pull request #5 from georgw777/develop
Browse files Browse the repository at this point in the history
v0.1.5
  • Loading branch information
georg-wolflein authored Dec 30, 2020
2 parents 0e2d72a + 25f59a6 commit 2cdbfaa
Show file tree
Hide file tree
Showing 16 changed files with 119 additions and 76 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# recap

![build](https://github.com/georgw777/recap/workflows/build/badge.svg)
![PyPI](https://img.shields.io/pypi/v/recap)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/recap)
![GitHub](https://img.shields.io/github/license/georgw777/recap)
[![build](https://github.com/georgw777/recap/workflows/build/badge.svg)](https://github.com/georgw777/recap/actions?query=workflow%3Abuild)
[![PyPI](https://img.shields.io/pypi/v/recap)](https://pypi.org/project/recap)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/recap)](https://pypi.org/project/recap)
[![Licence](https://img.shields.io/github/license/georgw777/recap)](https://github.com/georgw777/recap/blob/master/LICENSE)
[![Documentation Status](https://readthedocs.org/projects/recap/badge/?version=latest)](http://recap.readthedocs.io/?badge=latest)

_recap_ is a tool for providing _REproducible Configurations for Any Project_.

Expand Down
4 changes: 2 additions & 2 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.. mdinclude:: ../../README.md

.. toctree::
:maxdepth: 2
:maxdepth: 4
:caption: Contents:

modules
recap
7 changes: 0 additions & 7 deletions docs/source/modules.rst

This file was deleted.

13 changes: 5 additions & 8 deletions docs/source/recap.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
recap package
=============

.. automodule:: recap
:members:
:undoc-members:
:show-inheritance:

Submodules
----------

Expand All @@ -19,11 +24,3 @@ recap.path\_manager module
:members:
:undoc-members:
:show-inheritance:

Module contents
---------------

.. automodule:: recap
:members:
:undoc-members:
:show-inheritance:
4 changes: 2 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
[tool.poetry]
name = "recap"
version = "0.1.4"
version = "0.1.5"
description = "Reproducible configurations for any project"
authors = ["Georg Wölflein <[email protected]>"]
license = "Apache-2.0"
packages = [
{ include = "recap" }
]
readme = "README.md"
homepage = "https://github.com/georgw777/recap"
homepage = "http://recap.readthedocs.io"
repository = "https://github.com/georgw777/recap"
keywords = ["flake8", "markdown", "lint"]
classifiers = [
Expand All @@ -29,6 +29,7 @@ pyyaml = "^5.3.1"
sphinx = { version = "^3.4.1", optional = true }
sphinx-rtd-theme = { version = "^0.5.0", optional = true }
m2r2 = { version = "^0.2.7", optional = true }
wrapt = "^1.12.1"

[tool.poetry.dev-dependencies]
autopep8 = "^1.5.4"
Expand Down
3 changes: 3 additions & 0 deletions recap/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""The recap package. The most important classes, :class:`~recap.config.CfgNode` and :class:`~recap.path_manager.URI` are available as top-level exports.
"""

from .__version__ import __version__
from .config import CfgNode
from .path_manager import URI
2 changes: 1 addition & 1 deletion recap/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.1.4"
__version__ = "0.1.5"
9 changes: 8 additions & 1 deletion recap/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""Configuration management by extending :class:`yacs.config.CfgNode`.
"""

import os
from typing import Dict, Any
from pathlib import Path
Expand Down Expand Up @@ -41,7 +44,11 @@ def load_yaml_with_base(cls, filename: os.PathLike) -> "CfgNode":
with uri.open("r") as f:
cfg = cls.load_cfg(f)
if BASE_KEY in cfg:
base_cfg = cls.load_yaml_with_base(cfg[BASE_KEY])
base_uri = URI(cfg[BASE_KEY])
# If the URI of the base cfg is relative, it should be relative to the current YAML file
if not base_uri.is_absolute():
base_uri = uri.parent / base_uri
base_cfg = cls.load_yaml_with_base(base_uri)
del cfg[BASE_KEY]
base_cfg.merge_from_other_cfg(cfg)
return base_cfg
Expand Down
88 changes: 39 additions & 49 deletions recap/path_manager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""Module for conveniently managing paths through the :class:`URI` class which is fully compatible with :class:`pathlib.Path`.
"""

from pathlib import Path, PurePath, _PosixFlavour
from typing import Callable
import logging
Expand All @@ -6,6 +9,7 @@
from contextlib import contextmanager
import re
import os
import wrapt

logger = logging.getLogger(__name__)

Expand All @@ -24,6 +28,7 @@ def splitroot(self, part, sep=_PosixFlavour.sep):
root = ""
return drive, root, path
else:
self.has_drv = False
return super().splitroot(part, sep=sep)


Expand All @@ -49,18 +54,37 @@ def __repr__(self) -> str:
return "{}({!r})".format(self.__class__.__name__, s)


class PathTranslator(abc.ABC):
"""Abstract class representing a path translator that can translate a specific type of URIs to local paths.
"""

def __call__(self, uri: "URI") -> Path:
"""Translate a URI to a local path.
Usually, this involves looking at uri.path.
Args:
uri (URI): the URI
Returns:
Path: the corresponding local path
"""

raise NotImplementedError


class PathManagerBase:
"""Base class for a path manager.
This class simultaneously acts as a context manager for the currently active path manager of the URI class.
This class simultaneously acts as a context manager for the currently active path manager of the :class:`URI` class.
"""

def __init__(self):
self._handlers = {}
self._previous_path_managers = []

def resolve(self, path: os.PathLike) -> Path:
"""Resolve a path (which might be a URI) to a local path.
"""Resolve a path (which might be a :class:`pathlib.Path`) to a local path.
Args:
path (os.PathLike): the path
Expand All @@ -73,12 +97,13 @@ def resolve(self, path: os.PathLike) -> Path:
path = _URIBase(path)
if path.scheme:
if path.scheme not in self._handlers:
raise NotImplementedError(f"No handler is registered for scheme {path.scheme}")
raise NotImplementedError(
f"No handler is registered for scheme {path.scheme}")
return self._handlers[path.scheme](path)
else:
return Path(path.path)

def register_handler(self, scheme: str) -> callable:
def register_handler(self, scheme: str) -> Callable[[PathTranslator], PathTranslator]:
"""Decorator to register a path handler for a given URI scheme.
Args:
Expand All @@ -88,47 +113,28 @@ def register_handler(self, scheme: str) -> callable:
callable: the decorated function
"""

def decorator(func: Callable[[os.PathLike], Path]):
self._handlers[scheme] = func
def decorator(translator: PathTranslator) -> PathTranslator:
self._handlers[scheme] = translator
logger.debug(f"Registered path handler for scheme {scheme}")
return func
return translator
return decorator

def __enter__(self):
self._previous_path_managers.append(PathManager._instance)
PathManager._instance = self
self._previous_path_managers.append(PathManager.__wrapped__)
PathManager.__wrapped__ = self

def __exit__(self, exc_type, exc_val, exc_tb):
PathManager._instance = self._previous_path_managers.pop()


class PathManagerProxy(PathManagerBase):
"""Proxy class for the main path manager instance.
"""
PathManager.__wrapped__ = self._previous_path_managers.pop()

def __init__(self, instance: PathManagerBase):
self._instance = instance

def resolve(self, *args, **kwargs):
return self._instance.resolve(*args, **kwargs)

def register_handler(self, *args, **kwargs):
return self._instance.register_handler(*args, **kwargs)

def __enter__(self, *args, **kwargs):
return self._instance.__enter__(*args, **kwargs)

def __exit__(self, *args, **kwargs):
return self._instance.__exit__(*args, **kwargs)

#: The public path manager instance.
PathManager: PathManagerBase = PathManagerProxy(PathManagerBase())
PathManager: PathManagerBase = wrapt.ObjectProxy(PathManagerBase())


class URI(_URIBase):
"""A class representing a recap URI that is lazily evaluated to a local path when it is used.
It is fully compatible with pathlib.Path.
It is fully compatible with :class:`pathlib.Path`.
"""

def _init(self, *args, **kwargs):
Expand All @@ -138,24 +144,8 @@ def _init(self, *args, **kwargs):
def __str__(self) -> str:
return str(self._local_path)


class PathTranslator(abc.ABC):
"""Abstract class representing a path translator that can translate a specific type of URIs to local paths.
"""

def __call__(self, uri: URI) -> Path:
"""Translate a URI to a local path.
Usually, this involves looking at uri.path.
Args:
uri (URI): the URI
Returns:
Path: the corresponding local path
"""

raise NotImplementedError
def is_absolute(self) -> bool:
return self._local_path.is_absolute()


def register_translator(scheme: str, path: Path):
Expand Down
3 changes: 3 additions & 0 deletions tests/resources/data_type_override.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
_BASE_: ./data_type_override_base.yaml

VALUE: my string
1 change: 1 addition & 0 deletions tests/resources/data_type_override_base.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VALUE: 1234
2 changes: 2 additions & 0 deletions tests/resources/inherit_base.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TEST: 1
XYZ: abc
3 changes: 3 additions & 0 deletions tests/resources/inherit_override.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
_BASE_: inherit_base.yaml

TEST: 2
17 changes: 17 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pathlib import Path
import pytest

from recap import CfgNode as CN

RESOURCES = Path(__file__).parent / "resources"


def test_inherit_yaml():
cfg = CN.load_yaml_with_base(RESOURCES / "inherit_override.yaml")
assert cfg.TEST == 2
assert cfg.XYZ == "abc"


def test_data_type_override():
with pytest.raises(ValueError):
cfg = CN.load_yaml_with_base(RESOURCES / "data_type_override.yaml")
25 changes: 25 additions & 0 deletions tests/test_path_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ def test_uri_concat():
assert str(a) == "/path/a/b"


def test_path_manager_context():
assert len(PathManager._handlers) == 0
with PathManagerBase():
test_dir = Path("/a/b/test")
register_translator("test", test_dir)
assert len(PathManager._handlers) == 1
assert len(PathManager._handlers) == 0


def test_virtual_uri():
with PathManagerBase():
test_dir = Path("/a/b/test")
Expand Down Expand Up @@ -44,3 +53,19 @@ def abc(path: URI) -> Path:
return Path("/def") / path.path

assert str(manager.resolve("abc://d/e")) == str(Path("/def/d/e"))


def test_absolute_path():
assert URI("/a").is_absolute()


def test_relative_path():
assert not URI("a").is_absolute()


def test_absolute_uri():
with PathManagerBase():
test_dir = Path("/a/b/test")
register_translator("test", test_dir)
uri = URI("test://a/b")
assert uri.is_absolute()

0 comments on commit 2cdbfaa

Please sign in to comment.