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
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