Skip to content

Commit

Permalink
Rewrite the logic to resolve package-relative paths so we can remove …
Browse files Browse the repository at this point in the history
…the "asset" library (#247)

* implement the new function resolve_package_path
* add tests
provide tests for the new function resolve_package_path() and the
"include package()" statement in the config
* remove "asset" from the list of dependencies
* remove "import asset"
* compatibility with python 2.7
  • Loading branch information
klamann authored Nov 5, 2020
1 parent c2fba83 commit 2fffb09
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 20 deletions.
23 changes: 21 additions & 2 deletions pyhocon/config_parser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import codecs
import contextlib
import copy
import imp
import itertools
import logging
import os
Expand Down Expand Up @@ -29,7 +30,6 @@ def fixed_get_attr(self, item):

pyparsing.ParseResults.__getattr__ = fixed_get_attr

import asset
from pyhocon.config_tree import (ConfigInclude, ConfigList, ConfigQuotedString,
ConfigSubstitution, ConfigTree,
ConfigUnquotedString, ConfigValues, NoneValue)
Expand Down Expand Up @@ -349,7 +349,7 @@ def include_config(instring, loc, token):
if final_tokens[0] == 'url':
url = value
elif final_tokens[0] == 'package':
file = asset.load(value).filename
file = cls.resolve_package_path(value)
else:
file = value

Expand Down Expand Up @@ -712,6 +712,25 @@ def resolve_substitutions(cls, config, accept_unresolved=False):
cls._final_fixup(config)
return has_unresolved

@classmethod
def resolve_package_path(cls, package_path):
"""
Resolve the path to a file inside a Python package. Expected format: "PACKAGE:PATH"
Example: "my_package:foo/bar.conf" will resolve file 'bar.conf' in folder 'foo'
inside package 'my_package', which could result in a path like
'/path/to/.venv/lib/python3.7/site-packages/my_package/foo/bar.conf'
:param package_path: the package path, formatted as "PACKAGE:PATH"
:return: the absolute path to the specified file inside the specified package
"""
if ':' not in package_path:
raise ValueError("Expected format is 'PACKAGE:PATH'")
package_name, path_relative = package_path.split(':', 1)
package_dir = imp.find_module(package_name)[1]
path_abs = os.path.join(package_dir, path_relative)
return path_abs


class ListParser(TokenConverter):
"""Parse a list [elt1, etl2, ...]
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def run_tests(self):
packages=[
'pyhocon',
],
install_requires=['pyparsing>=2.0.3', 'asset'],
install_requires=['pyparsing>=2.0.3'],
extras_require={
'Duration': ['python-dateutil>=2.8.0']
},
Expand Down
50 changes: 33 additions & 17 deletions tests/test_config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import json
import os
import shutil
import tempfile
from collections import OrderedDict
from datetime import timedelta

from pyparsing import ParseBaseException, ParseException, ParseSyntaxException
import asset
import mock
import pytest
from pyhocon import (ConfigFactory, ConfigParser, ConfigSubstitutionException, ConfigTree)
Expand Down Expand Up @@ -1267,26 +1267,42 @@ def test_include_missing_required_file(self):
"""
)

def test_include_asset_file(self, monkeypatch):
with tempfile.NamedTemporaryFile('w') as fdin:
fdin.write('{a: 1, b: 2}')
fdin.flush()

def load(*args, **kwargs):
class File(object):
def __init__(self, filename):
self.filename = filename

return File(fdin.name)

monkeypatch.setattr(asset, "load", load)

def test_resolve_package_path(self):
path = ConfigParser.resolve_package_path("pyhocon:config_parser.py")
assert os.path.exists(path)

def test_resolve_package_path_format(self):
with pytest.raises(ValueError):
ConfigParser.resolve_package_path("pyhocon/config_parser.py")

def test_resolve_package_path_missing(self):
with pytest.raises(ImportError):
ConfigParser.resolve_package_path("non_existent_module:foo.py")

def test_include_package_file(self, monkeypatch):
temp_dir = tempfile.mkdtemp()
try:
module_dir = os.path.join(temp_dir, 'my_module')
module_conf = os.path.join(module_dir, 'my.conf')
# create the module folder and necessary files (__init__ and config)
os.mkdir(module_dir)
open(os.path.join(module_dir, '__init__.py'), 'a').close()
with open(module_conf, 'w') as fdin:
fdin.write("{c: 3}")
# add the temp dir to sys.path so that 'my_module' can be discovered
monkeypatch.syspath_prepend(temp_dir)
# load the config and include the other config file from 'my_module'
config = ConfigFactory.parse_string(
"""
include package("dotted.name:asset/config_file")
a: 1
b: 2
include package("my_module:my.conf")
"""
)
assert config['a'] == 1
# check that the contents of both config files are available
assert dict(config.as_plain_ordered_dict()) == {'a': 1, 'b': 2, 'c': 3}
finally:
shutil.rmtree(temp_dir, ignore_errors=True)

def test_include_dict(self):
expected_res = {
Expand Down

0 comments on commit 2fffb09

Please sign in to comment.